Skip to content

Commit

Permalink
Optimize performance of results tree
Browse files Browse the repository at this point in the history
The GUI is very slow when the number of tests exceeds 500. The reason
for this is the tree component that displays the test names. Its
implementation is inefficient. In each frame, this component draws all
its content - even those invisible to the user.

This commit changes the implementation of the tree component to a more
efficient one. The new implementation only draws the part of the tree
that is currently presented to the user. This component also uses a
minimal amount of RAM, which allows you to run several thousand tests.
  • Loading branch information
elgopher committed Nov 24, 2024
1 parent 6d38619 commit 9a30712
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 114 deletions.
25 changes: 18 additions & 7 deletions gui/gui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

-- this file contains code controlling GUI of test runner

include "gui/tree.lua"
include "gui/text_output.lua"
include "gui/lights.lua"
include "gui/test_summary.lua"
include "gui/test_toolbar.lua"
include "gui/printed_lines.lua"
include "gui/tree.lua"

local width <const> = 280
local height <const> = 200
Expand Down Expand Up @@ -65,12 +65,17 @@ local function start_test(item)
y = 97,
width = width,
height = 103,
bg_color = 0,
lines_len = function()
if selected_test_id == nil then return 0 end
return printed_lines:lines_len(selected_test_id)
end,
get_line = function(line_no)
return printed_lines:line(selected_test_id, line_no)
return {
text = printed_lines:line(selected_test_id, line_no),
bg_color = 0,
fg_color = 7,
}
end,
is_link = function(line_no)
local text = printed_lines:line(selected_test_id, line_no)
Expand Down Expand Up @@ -100,7 +105,7 @@ local function start_test(item)
local function select_test(test_id)
selected_test_id = test_id

text_output:scroll_to_the_top()
text_output:scroll_to_line(1)

lights:detach()
test_summary:detach()
Expand All @@ -122,11 +127,14 @@ local function start_test(item)

test_tree = attach_tree(
gui,
{ x = 0, y = 16, width = width, height = 80 }
{
x = 0,
y = 16,
width = width,
height = 80,
select = select_test
}
)
function test_tree:select(e)
select_test(e.id)
end

attach_toolbar(
gui,
Expand Down Expand Up @@ -328,5 +336,8 @@ function _draw()
if gui != nil then
cls()
gui:draw_all()
-- debug fps and memory usage:
-- print(stat(7), 250, 0, 1)
-- print(stat(0), 220, 10, 1)
end
end
23 changes: 18 additions & 5 deletions gui/text_output.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-- (c) 2024 Jacek Olszak
-- This code is licensed under MIT license (see LICENSE for details)

---@param el {x:number, y:number, width:number, height:number, is_link:function, link_click:function, get_line:function, lines_len:function}
---@param el {x:number, y:number, width:number, height:number, bg_color:integer, is_link:function, link_click:function, get_line:function, lines_len:function}
function attach_text_output(parent, el)
local line_height <const> = 10

Expand Down Expand Up @@ -51,19 +51,32 @@ function attach_text_output(parent, el)

for i = line_no, last_line do
local line = el.get_line(i + 1)
print(line, 0, i * line_height, 7)
rectfill(0, i * line_height,
text_output.width, (i + 1) * line_height + 1,
line.bg_color)
print(line.text, 1, i * line_height + 1, line.fg_color)
end
end

function text_output:mousewheel(e)
self.y += e.wheel_y * 32
end

function el:scroll_to_the_top()
text_output.y = 0
function el:scroll_to_line(line_no)
text_output.y = -(line_height * (line_no - 1))
if text_output.y == 0 then
return
end
text_output.y += el.height / 2 - line_height
if -text_output.y + el.height > text_output.height then
text_output.y = -text_output.height + el.height
end
end

function el:draw() end
function el:draw()
-- draw background (needed when tree height is too low)
rectfill(0, 0, el.width, el.height, el.bg_color)
end

return el
end
154 changes: 52 additions & 102 deletions gui/tree.lua
Original file line number Diff line number Diff line change
@@ -1,120 +1,70 @@
--[[pod_format="raw",created="2024-11-09 17:24:35",modified="2024-11-09 17:24:35",revision=0]]
-- (c) 2024 Jacek Olszak
-- This code is licensed under MIT license (see LICENSE for details)

---@param el {x:number,y:number,width:number,height:number,select:function}
function attach_tree(parent_el, el)
local bg_color <const> = 7
local fg_color <const> = 13
local highlight_bg_color <const> = 1
local highlight_fg_color <const> = 7

local node_height <const> = 10

local nodes_by_id <const> = {}

local selected_child

el = parent_el:attach(el)

local root_node = el:attach(
{ width = el.width, height = 0, x = 0, y = 0 }
)
root_node.indent = ""
el:attach_scrollbars { autohide = true }
include "gui/tree_provider.lua"

local provider <const> = new_tree_provider()

local selected_line = nil

local tree = attach_text_output(parent_el, {
x = el.x,
y = el.y,
width = el.width,
height = el.height,
bg_color = 7,
get_line = function(line_no)
local node = provider:get_node(line_no)
local whitespace = " "

local fg_color = 13
local bg_color = 7
if selected_line != nil and selected_line == line_no then
fg_color = 7
bg_color = 1
end

function el:add_child(id, text, parent_id)
local parent
if parent_id == nil then
parent = root_node
else
parent = nodes_by_id[parent_id]
end
local prefix = whitespace:rep(node.depth * 2)
if node.has_children then
prefix = prefix .. "[-] "
else
prefix = prefix .. " "
end

local text = prefix .. node.text

local child = parent:attach(
{
width = parent.width,
height = node_height,
x = 0,
y = parent.height
return {
text = text,
bg_color = bg_color,
fg_color = fg_color,
}
)
child.text = text
child.indent = parent.indent .. " "
if parent_id == nil then
child.indent = "" -- root element should not have an indent
end
function child:draw(msg)
if child == selected_child then
pal(bg_color, highlight_bg_color);
pal(fg_color, highlight_fg_color)
end
local prefix = "[-] "
if #child.child == 0 then
prefix = " "
end
rectfill(0, 0, self.width, node_height, bg_color)
print(child.indent .. prefix .. self.text, 0, 1, fg_color)
pal()
end

function child:click()
selected_child = child
el:select { id = id }
end,
is_link = function(line_no)
return true
end,
link_click = function(line_no)
selected_line = line_no
el.select(provider:get_node(line_no).id)
end,
lines_len = function()
return provider:nodes_len()
end
})

local function add_height(parent, h)
parent.height += h
if parent._parent != nil then
add_height(parent._parent, h)
end
end

add_height(parent, node_height)

-- use _parent beause parent is already used by Picotron:
child._parent = parent
nodes_by_id[id] = child
child.cursor = "pointer"
end

function el:draw()
rectfill(0, 0, el.width, el.height, bg_color)
end

function el:update_child_text(id, text)
nodes_by_id[id].text = text
function tree:add_child(id, text, parent_id)
provider:append_node(parent_id, id, text)
end

local function child_y_relative_to_root_node(child)
local y = child.y
while child.parent != root_node do
y += child.parent.y
child = child.parent
end
return y
end

local function scroll_to_child(child)
local scroll_root_node =
-child_y_relative_to_root_node(child) + (el.height / 2)
if scroll_root_node > 0 then
scroll_root_node = 0
end
local max_scroll = -root_node.height + el.height
if scroll_root_node <= max_scroll then
scroll_root_node = max_scroll
end

root_node.y = scroll_root_node
function tree:update_child_text(id, text)
provider:update_node_text(id, text)
end

function el:select_child(id)
selected_child = nodes_by_id[id]
scroll_to_child(selected_child)
function tree:select_child(id)
local line = provider:get_line_no(id)
selected_line = line
tree:scroll_to_line(line)
end

return el
return tree
end
48 changes: 48 additions & 0 deletions gui/tree_provider.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- (c) 2024 Jacek Olszak
-- This code is licensed under MIT license (see LICENSE for details)

--- returns a new instance of data structure holding tree of nodes. This object
--- is used by tree component. The code was extracted from the tree component
--- because the component was to complex (and will be even more complex
--- when tree will have node collapsing and hiding functionality).
function new_tree_provider()
local p = {}

local nodes_by_line <const> = {}
local nodes_by_id <const> = {}

function p:nodes_len()
return #nodes_by_line
end

function p:get_node(line_no)
return nodes_by_line[line_no]
end

function p:get_line_no(id)
for line_no, node in ipairs(nodes_by_line) do
if node.id == id then
return line_no
end
end

return nil
end

function p:append_node(parent_id, id, text)
local node = { text = text, depth = 0, id = id, has_children = false }
if parent_id != nil then
local parent = nodes_by_id[parent_id]
parent.has_children = true
node.depth = parent.depth + 1
end
nodes_by_id[node.id] = node
table.insert(nodes_by_line, node)
end

function p:update_node_text(id, new_text)
nodes_by_id[id].text = new_text
end

return p
end
39 changes: 39 additions & 0 deletions gui/tree_provider_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
-- (c) 2024 Jacek Olszak
-- This code is licensed under MIT license (see LICENSE for details)

include "tree_provider.lua"

test("new provider has 0 lines", function()
local p = new_tree_provider()
assert_eq(0, p:nodes_len())
end)

test("add root", function()
local p = new_tree_provider()
p:append_node(nil, 1, "root")
assert_eq(1, p:nodes_len())
assert_eq({ text = "root", depth = 0, id = 1, has_children = false }, p:get_node(1))
assert_eq(1, p:get_line_no(1))
end)

test("add child node", function()
local p = new_tree_provider()
p:append_node(nil, 1, "root")
-- when
p:append_node(1, 2, "child")
-- then
assert_eq(2, p:nodes_len())
assert_eq({ text = "child", depth = 1, id = 2, has_children = false }, p:get_node(2))
assert_eq(2, p:get_line_no(2))
-- and
assert(p:get_node(1).has_children)
end)

test("update node text", function()
local p = new_tree_provider()
p:append_node(nil, 1, "root")
-- when
p:update_node_text(1, "updated")
-- then
assert_eq("updated", p:get_node(1).text)
end)

0 comments on commit 9a30712

Please sign in to comment.