diff options
| author | ruki <[email protected]> | 2025-11-16 00:16:02 +0800 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-11-16 00:16:02 +0800 |
| commit | 6869f1541f155dcc240f0a0a2de9e076f377856a (patch) | |
| tree | 2f5eebebee3ae0ead836f10028f4865507d444ab | |
| parent | 100900c957fa0db180ccaf5f1f157a78e9db665d (diff) | |
| parent | 690f91a4b41a78adda91f1c6d1a7aed559cd3bc4 (diff) | |
Merge pull request #7027 from xmake-io/graph
Improve graph
| -rw-r--r-- | tests/modules/graph/test.lua | 42 | ||||
| -rw-r--r-- | tests/modules/jobgraph/test.lua | 45 | ||||
| -rw-r--r-- | xmake/core/base/graph.lua | 93 | ||||
| -rw-r--r-- | xmake/modules/async/jobgraph.lua | 95 |
4 files changed, 237 insertions, 38 deletions
diff --git a/tests/modules/graph/test.lua b/tests/modules/graph/test.lua index fe1d63e8d..7c17a4fec 100644 --- a/tests/modules/graph/test.lua +++ b/tests/modules/graph/test.lua @@ -170,6 +170,48 @@ function test_paritail_topo_sort_dynamic(t) end end +function test_remove_edge_and_vertex(t) + local gh = graph.new(true) + gh:add_edge("a", "b") + gh:add_edge("b", "c") + gh:add_edge("c", "d") + gh:add_edge("a", "d") + + t:require(gh:has_edge("a", "b")) + gh:remove_edge("a", "b") + t:require(not gh:has_edge("a", "b")) + t:require(gh:has_edge("a", "d")) + + gh:remove_vertex("c") + t:require(not gh:has_edge("b", "c")) + t:require(not gh:has_edge("c", "d")) + t:are_equal(#gh:vertices(), 3) + local order = gh:topo_sort() + t:require(#order == 3) + + gh:add_edge("b", "a") + gh:add_edge("d", "b") + local _, has_cycle = gh:topo_sort() + t:require(has_cycle) +end + +function test_clone_reverse_undirected(t) + local ug = graph.new(false) + ug:add_edge(1, 2) + ug:add_edge(2, 3) + ug:add_edge(3, 1) + + local clone = ug:clone() + t:require(#clone:edges() == #ug:edges()) + t:require(clone:has_edge(1, 2)) + t:require(clone:has_edge(2, 1)) + + local rev = ug:reverse() + t:require(rev:has_edge(1, 2)) + t:require(rev:has_edge(2, 1)) + t:require(#rev:edges() == #ug:edges()) +end + function test_find_cycle(t) local edges = { {9, 1}, diff --git a/tests/modules/jobgraph/test.lua b/tests/modules/jobgraph/test.lua new file mode 100644 index 000000000..ba01d38da --- /dev/null +++ b/tests/modules/jobgraph/test.lua @@ -0,0 +1,45 @@ +import("async.jobgraph") + +local function dummy_job() end + +function test_group_bridge_reuse(t) + local jobs = jobgraph.new() + jobs:group("foo", function () + jobs:add("foo/1", dummy_job) + jobs:add("foo/2", dummy_job) + end) + jobs:group("bar", function () + jobs:add("bar/1", dummy_job) + end) + jobs:add_orders("foo", "bar") + local vertices_before = #jobs._dag:vertices() + jobs:add_orders("foo", "bar") + local vertices_after = #jobs._dag:vertices() + t:are_equal(vertices_before, vertices_after) +end + +function test_group_bridge_updates_with_new_job(t) + local jobs = jobgraph.new() + jobs:group("foo", function () + jobs:add("foo/1", dummy_job) + end) + jobs:group("bar", function () + jobs:add("bar/1", dummy_job) + end) + jobs:add_orders("foo", "bar") + jobs:group("foo", function () + jobs:add("foo/2", dummy_job) + end) + + local queue = jobs:build() + local first = queue:getfree() + t:require(first.name == "foo/1" or first.name == "foo/2") + queue:remove(first) + local second = queue:getfree() + t:require(second.name == "foo/1" or second.name == "foo/2") + t:require(second.name ~= first.name) + queue:remove(second) + local third = queue:getfree() + t:are_equal(third.name, "bar/1") +end + diff --git a/xmake/core/base/graph.lua b/xmake/core/base/graph.lua index 64dfa3b31..be0035394 100644 --- a/xmake/core/base/graph.lua +++ b/xmake/core/base/graph.lua @@ -116,20 +116,18 @@ function graph:remove_vertex(v) end end) if contains then - self._edges_map[v] = nil - self._adjacent_edges[v] = nil - -- remove the adjacent edge with this vertex in the other vertices - for _, w in ipairs(self:vertices()) do - local edges = self:adjacent_edges(w) - if edges then - table.remove_if(edges, function (_, e) - if e:other(w) == v then - self._edges_map[w] = nil - return true - end - end) + -- remove all touching edges + local touching = {} + for _, e in ipairs(self._edges) do + if e:from() == v or e:to() == v then + table.insert(touching, {e:from(), e:to()}) end end + for _, pair in ipairs(touching) do + self:remove_edge(pair[1], pair[2]) + end + self._edges_map[v] = nil + self._adjacent_edges[v] = nil -- reset partial topological sort state since graph structure changed self._partial_topo_dirty = true @@ -398,6 +396,59 @@ function graph:add_edge(from, to) self._partial_topo_dirty = true end +-- remove edge +function graph:remove_edge(from, to) + if not self:has_edge(from, to) then + return + end + local directed = self:is_directed() + local edges = self._edges + local target_index + local target_edge + for idx, e in ipairs(edges) do + local match = (e:from() == from and e:to() == to) + if not directed then + match = match or (e:from() == to and e:to() == from) + end + if match then + target_index = idx + target_edge = e + break + end + end + assert(target_edge, string.format("graph.remove_edge(%s, %s): edge not found", tostring(from), tostring(to))) + + table.remove(edges, target_index) + + local function remove_from_adj(vertex) + local adj = self._adjacent_edges[vertex] + if adj then + table.remove_if(adj, function (_, e) return e == target_edge end) + end + end + + remove_from_adj(target_edge:from()) + remove_from_adj(target_edge:to()) + + local edges_map = self._edges_map + local function clear_map(u, v) + local map = edges_map[u] + if map then + map[v] = nil + if not next(map) then + edges_map[u] = nil + end + end + end + + clear_map(target_edge:from(), target_edge:to()) + if not directed then + clear_map(target_edge:to(), target_edge:from()) + end + + self._partial_topo_dirty = true +end + -- has the given edge? function graph:has_edge(from, to) local edges_map = self._edges_map @@ -413,13 +464,8 @@ end -- clone graph function graph:clone() local gh = graph.new(self:is_directed()) - for _, v in ipairs(self:vertices()) do - local edges = self:adjacent_edges(v) - if edges then - for _, e in ipairs(edges) do - gh:add_edge(e:from(), e:to()) - end - end + for _, e in ipairs(self._edges) do + gh:add_edge(e:from(), e:to()) end return gh end @@ -430,13 +476,8 @@ function graph:reverse() return self:clone() end local gh = graph.new(self:is_directed()) - for _, v in ipairs(self:vertices()) do - local edges = self:adjacent_edges(v) - if edges then - for _, e in ipairs(edges) do - gh:add_edge(e:to(), e:from()) - end - end + for _, e in ipairs(self._edges) do + gh:add_edge(e:to(), e:from()) end return gh end diff --git a/xmake/modules/async/jobgraph.lua b/xmake/modules/async/jobgraph.lua index 67fe836c1..a1abbf86d 100644 --- a/xmake/modules/async/jobgraph.lua +++ b/xmake/modules/async/jobgraph.lua @@ -26,7 +26,51 @@ import("core.base.hashset") -- define module local jobqueue = jobqueue or object {_init = {"_jobgraph", "_dag"}} -local jobgraph = jobgraph or object {_init = {"_name", "_jobs", "_size", "_dag", "_groups"}} +local jobgraph = jobgraph or object {_init = {"_name", "_jobs", "_size", "_dag", "_groups", "_bridge_nodes", "_bridge_from", "_bridge_to"}} + +-- attach a job to existing bridge nodes of its group (if any) +function jobgraph:_attach_job_to_bridges(job, group_name) + local outbound = self._bridge_from[group_name] + local inbound = self._bridge_to[group_name] + if not outbound and not inbound then + return + end + if outbound then + for _, bridge in ipairs(outbound) do + self._dag:add_edge(job, bridge) + end + end + if inbound then + for _, bridge in ipairs(inbound) do + self._dag:add_edge(bridge, job) + end + end +end + +-- ensure a reusable bridge node exists between two groups +function jobgraph:_ensure_bridge(from_group, to_group) + local key = from_group .. "->" .. to_group + local bridge = self._bridge_nodes[key] + if bridge then + return bridge + end + bridge = {from_group = from_group, to_group = to_group} + self._bridge_nodes[key] = bridge + self._dag:add_vertex(bridge) + local from_members = self._groups[from_group] or {} + for _, member in ipairs(from_members) do + self._dag:add_edge(member, bridge) + end + local to_members = self._groups[to_group] or {} + for _, member in ipairs(to_members) do + self._dag:add_edge(bridge, member) + end + self._bridge_from[from_group] = self._bridge_from[from_group] or {} + table.insert(self._bridge_from[from_group], bridge) + self._bridge_to[to_group] = self._bridge_to[to_group] or {} + table.insert(self._bridge_to[to_group], bridge) + return bridge +end -- remove the finished job function jobqueue:remove(job) @@ -79,15 +123,37 @@ function jobgraph:add(name, run, opt) self._size = self._size + 1 if self._current_groups or opt.groups then - local job_groups = table.join(self._current_groups or {}, opt.groups) - for _, group_name in ipairs(job_groups) do + local seen + local job_groups + local function add_group(group_name) + if not group_name then + return + end + seen = seen or {} + if seen[group_name] then + return + end + seen[group_name] = true + job_groups = job_groups or {} + table.insert(job_groups, group_name) local groups = self._groups[group_name] if not groups then groups = {} self._groups[group_name] = groups end table.insert(groups, job) + -- ensure bridges (if any) are updated for new members + self:_attach_job_to_bridges(job, group_name) + end + for _, group_name in ipairs(self._current_groups or {}) do + add_group(group_name) + end + if opt.groups then + for _, group_name in ipairs(opt.groups) do + add_group(group_name) + end end + job._groups = job_groups end else wprint("job(%s): has already been added!", name) @@ -103,6 +169,17 @@ function jobgraph:remove(name) assert(self._size > 0) jobs[name] = nil dag:remove_vertex(job) + if job._groups then + for _, group_name in ipairs(job._groups) do + local members = self._groups[group_name] + if members then + table.remove_if(members, function (_, item) return item == job end) + if #members == 0 then + self._groups[group_name] = nil + end + end + end + end self._size = self._size - 1 end end @@ -164,14 +241,8 @@ function jobgraph:add_orders(...) assert(curr, "job(%s) not found in jobgraph(%s)", name, self) if prev then if prev_is_group and curr_is_group then - -- we use a bridge job as a node to bridge the two groups. - local bridge = {from_group = prev_name, to_group = name} - for _, job in ipairs(prev) do - dag:add_edge(job, bridge) - end - for _, job in ipairs(curr) do - dag:add_edge(bridge, job) - end + -- we use (and reuse) a bridge job as a node to bridge the two groups. + self:_ensure_bridge(prev_name, name) elseif curr_is_group then for _, job in ipairs(curr) do dag:add_edge(prev, job) @@ -249,5 +320,5 @@ end -- new a jobgraph function new(name) - return jobgraph {name, {}, 0, graph.new(true), {}} + return jobgraph {name, {}, 0, graph.new(true), {}, {}, {}, {}} end |
