From d15266b6d5e0e4e71174255bdc05370da88979c0 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 12 Mar 2025 15:30:56 +0100 Subject: [PATCH] Devtools initial commit --- .gitignore | 4 + .luaurc | 3 +- benches/visual/despawn.bench.luau | 63 ++++++--- demo/src/ReplicatedStorage/track.luau | 48 +++++++ test/devtools_test.luau | 20 +++ tools/lifetime_tracker.luau | 193 ++++++++++++++++++++++++++ 6 files changed, 308 insertions(+), 23 deletions(-) create mode 100644 demo/src/ReplicatedStorage/track.luau create mode 100644 test/devtools_test.luau create mode 100644 tools/lifetime_tracker.luau diff --git a/.gitignore b/.gitignore index 322caff..a522b3c 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,7 @@ drafts/ # Luau tools profile.* + +# Patch files + +*.patch diff --git a/.luaurc b/.luaurc index 07221f7..e3c9db3 100644 --- a/.luaurc +++ b/.luaurc @@ -2,7 +2,8 @@ "aliases": { "jecs": "jecs", "testkit": "test/testkit", - "mirror": "mirror" + "mirror": "mirror", + "tools": "tools" }, "languageMode": "strict" } diff --git a/benches/visual/despawn.bench.luau b/benches/visual/despawn.bench.luau index 8bcf1dc..5c424d9 100644 --- a/benches/visual/despawn.bench.luau +++ b/benches/visual/despawn.bench.luau @@ -5,41 +5,60 @@ local ReplicatedStorage = game:GetService("ReplicatedStorage") local Matter = require(ReplicatedStorage.DevPackages.Matter) local ecr = require(ReplicatedStorage.DevPackages.ecr) local jecs = require(ReplicatedStorage.Lib) +local pair = jecs.pair local newWorld = Matter.World.new() local ecs = jecs.World.new() +local mirror = require(ReplicatedStorage.mirror) +local mcs = mirror.World.new() -local A, B = Matter.component(), Matter.component() -local C, D = ecs:component(), ecs:component() +local C1 = ecs:component() +local C2 = ecs:entity() +ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete)) +local C3 = ecs:entity() +ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete)) +local C4 = ecs:entity() +ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E1 = mcs:component() +local E2 = mcs:entity() +mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E3 = mcs:entity() +mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete)) +local E4 = mcs:entity() +mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete)) + +local registry2 = ecr.registry() return { ParameterGenerator = function() - local matter_entities = {} - local jecs_entities = {} - local entities = { - matter = matter_entities, - jecs = jecs_entities, - } + local j = ecs:entity() + ecs:set(j, C1, true) + local m = mcs:entity() + mcs:set(m, E1, true) for i = 1, 1000 do - table.insert(matter_entities, newWorld:spawn(A(), B())) - local e = ecs:entity() - ecs:set(e, C, {}) - ecs:set(e, D, {}) - table.insert(jecs_entities, e) + local friend1 = ecs:entity() + local friend2 = mcs:entity() + + ecs:add(friend1, pair(C2, j)) + ecs:add(friend1, pair(C3, j)) + ecs:add(friend1, pair(C4, j)) + + mcs:add(friend2, pair(E2, m)) + mcs:add(friend2, pair(E3, m)) + mcs:add(friend2, pair(E4, m)) end - return entities + return { + m = m, + j = j, + } end, Functions = { - Matter = function(_, entities) - for _, entity in entities.matter do - newWorld:despawn(entity) - end + Mirror = function(_, a) + mcs:delete(a.m) end, - Jecs = function(_, entities) - for _, entity in entities.jecs do - ecs:delete(entity) - end + Jecs = function(_, a) + ecs:delete(a.j) end, }, } diff --git a/demo/src/ReplicatedStorage/track.luau b/demo/src/ReplicatedStorage/track.luau new file mode 100644 index 0000000..1214a78 --- /dev/null +++ b/demo/src/ReplicatedStorage/track.luau @@ -0,0 +1,48 @@ +local events = {} + +local function trackers_invoke(event, component, entity, ...) + local trackers = events[event][component] + if not trackers then + return + end + + for _, tracker in trackers do + tracker(entity, data) + end +end + +local function trackers_init(event, component, fn) + local ob = events[event] + + return { + connect = function(component, fn) + local trackers = ob[component] + if not trackers then + trackers = {} + ob[component] = trackers + end + + table.insert(trackers, fn) + end, + invoke = function(component, ...) + trackers_invoke(event, component, ...) + end + } + return function(component, fn) + local trackers = ob[component] + if not trackers then + trackers = {} + ob[component] = trackers + end + + table.insert(trackers, fn) + end +end + +local trackers = { + emplace = trackers_init("emplace"), + add = trackers_init("added"), + remove = trackers_init("removed") +} + +return trackers diff --git a/test/devtools_test.luau b/test/devtools_test.luau new file mode 100644 index 0000000..e227c70 --- /dev/null +++ b/test/devtools_test.luau @@ -0,0 +1,20 @@ +local jecs = require("@jecs") +local lifetime_tracker_add = require("@tools/lifetime_tracker") +local world = lifetime_tracker_add(jecs.world()) +world:print_snapshot() +local e = world:entity() +local e1 = world:entity() +world:delete(e) + +world:print_snapshot() +local e2 = world:entity() +local e3 = world:entity() +world:print_snapshot() +world:delete(e1) +world:delete(e2) +world:delete(e3) +world:print_snapshot() +world:print_entities() +world:entity() +world:entity() +world:print_snapshot() diff --git a/tools/lifetime_tracker.luau b/tools/lifetime_tracker.luau new file mode 100644 index 0000000..c80d133 --- /dev/null +++ b/tools/lifetime_tracker.luau @@ -0,0 +1,193 @@ +local jecs = require("@jecs") +local ECS_GENERATION = jecs.ECS_GENERATION +local ECS_ID = jecs.ECS_ID +local __ = jecs.Wildcard +local pair = jecs.pair + +local testkit = require("@testkit") +local BENCH, START = testkit.benchmark() + +local it = testkit.test() +local TEST, CASE = it.TEST, it.CASE +local CHECK, FINISH = it.CHECK, it.FINISH +local SKIP, FOCUS = it.SKIP, it.FOCUS +local CHECK_EXPECT_ERR = it.CHECK_EXPECT_ERR + + +local c = { + white_underline = function(s: any) + return `\27[1;4m{s}\27[0m` + end, + + white = function(s: any) + return `\27[37;1m{s}\27[0m` + end, + + green = function(s: any) + return `\27[32;1m{s}\27[0m` + end, + + red = function(s: any) + return `\27[31;1m{s}\27[0m` + end, + + yellow = function(s: any) + return `\27[33;1m{s}\27[0m` + end, + + red_highlight = function(s: any) + return `\27[41;1;30m{s}\27[0m` + end, + + green_highlight = function(s: any) + return `\27[42;1;30m{s}\27[0m` + end, + + gray = function(s: any) + return `\27[30;1m{s}\27[0m` + end, +} + +local function pe(e: any) + local gen = ECS_GENERATION(e) + return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`) +end + +function print_centered_entity(entity, width: number) + local entity_str = tostring(entity) + local entity_length = #entity_str + + -- Calculate total padding needed to center the string + local padding_total = width - 2 - entity_length -- Subtract 2 for the `| |` characters + + -- Calculate padding for the left and right + local padding_left = math.floor(padding_total / 2) + local padding_right = padding_total - padding_left + + -- Build the centered string + local centered_str = string.rep(" ", padding_left) .. entity_str .. string.rep(" ", padding_right) + + -- Print with pipes around the centered string + print("|" .. centered_str .. "|") +end + +local function lifetime_tracker_add(world: jecs.World) + local entity_index = world.entity_index + local dense_array = entity_index.dense_array + local world_delete = world.delete + local world_entity = world.entity + local component_index = world.component_index + + local ENTITY_RANGE = (jecs.Rest :: any) + 1 + + local w = setmetatable({}, { __index = world }) + w.delete = function(self, e) + print("Entity deleted:", e) + + for child in world:each(pair(__, e)) do + + end + return world_delete(world, e) + end + w.entity = function(self) + local e = world_entity(world) + print("Entity created:", pe(e)) + return e + end + w.print_entities = function(self) + local max_id = entity_index.max_id + local alive_count = entity_index.alive_count + local alive = table.move(dense_array, 1+jecs.Rest::any, alive_count, 1, {}) + local dead = table.move(dense_array, alive_count + 1, max_id, 1, {}) + + local sep = "|--------|" + print("|-alive--|") + for i = 1, #alive do + local e = pe(alive[i]) + print_centered_entity(e, 32) + print(sep) + end + print("\n") + print("|--dead--|") + for i = 1, #dead do + print_centered_entity(pe(dead[i]), 32) + print(sep) + end + end + local timelines = {} + w.print_snapshot = function(self) + local timeline = #timelines + 1 + local entity_column_width = 10 + local status_column_width = 8 + + local header = string.format("| %-" .. entity_column_width .. "s |", "Entity") + for i = 1, timeline do + header = header .. string.format(" %-" .. status_column_width .. "s |", string.format("T%d", i)) + end + + local max_id = entity_index.max_id + local alive_count = entity_index.alive_count + local alive = table.move(dense_array, 1+jecs.Rest::any, alive_count, 1, {}) + local dead = table.move(dense_array, alive_count + 1, max_id, 1, {}) + + local data = {} + print("-------------------------------------------------------------------") + print(header) + + -- Store the snapshot data for this timeline + for i = ENTITY_RANGE, max_id do + if dense_array[i] then + local entity = dense_array[i] + local id = ECS_ID(entity) + local status = "alive" + if id > alive_count then + status = "dead" + end + data[id] = status + end + end + + table.insert(timelines, data) + + -- Create a table to hold entity data for sorting + local entities = {} + for i = ENTITY_RANGE, max_id do + if dense_array[i] then + local entity = dense_array[i] + local id = ECS_ID(entity) + -- Push entity and id into the new `entities` table + table.insert(entities, {entity = entity, id = id}) + end + end + + -- Sort the entities by ECS_ID + table.sort(entities, function(a, b) + return a.id < b.id + end) + + -- Print the sorted rows + for _, entity_data in ipairs(entities) do + local entity = entity_data.entity + local id = entity_data.id + local status = "alive" + if id > alive_count then + status = "dead" + end + local row = string.format("| %-" .. entity_column_width .. "s |", pe(entity)) + for j = 1, timeline do + local timeline_data = timelines[j] + local entity_data = timeline_data[id] + if entity_data then + row = row .. string.format(" %-" .. status_column_width .. "s |", entity_data) + else + row = row .. string.format(" %-" .. status_column_width .. "s |", "-") + end + end + print(row) + end + print("-------------------------------------------------------------------") + end + return w +end + +return lifetime_tracker_add