From 38d631453b6f4659eea64be961f72586e00aa92b Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 16 Sep 2025 14:46:06 +0200 Subject: [PATCH] Allow 0-term cached queries --- benches/ecsgame.luau | 201 ++++++++ benches/smallgame.luau | 213 +++++++++ examples/luau/hooks/traits/isa.luau | 46 ++ perf.svg | 654 ++++++++++++++++++++++++++ perf2.svg | 430 +++++++++++++++++ tools/__pycache__/svg.cpython-312.pyc | Bin 0 -> 20032 bytes 6 files changed, 1544 insertions(+) create mode 100755 benches/ecsgame.luau create mode 100755 benches/smallgame.luau create mode 100755 examples/luau/hooks/traits/isa.luau create mode 100755 perf.svg create mode 100755 perf2.svg create mode 100755 tools/__pycache__/svg.cpython-312.pyc diff --git a/benches/ecsgame.luau b/benches/ecsgame.luau new file mode 100755 index 0000000..17c3559 --- /dev/null +++ b/benches/ecsgame.luau @@ -0,0 +1,201 @@ +local jecs = require("@jecs") + +local world = jecs.world() +local Physics = world:entity() +local DamagableByPlayer = world:entity() +local Mob = world:entity() +local Player = world:entity() +local Goblin = world:entity() + +local Position = world:component() :: jecs.Id +local Velocity = world:component() :: jecs.Id +local Acceleration = world:component() :: jecs.Id + +local Health = world:component() :: jecs.Id +local MaxHealth = world:component() :: jecs.Id + +local TouchHurts = world:entity() + + +local function setup_player(entity: jecs.Entity) + jecs.bulk_insert(world, entity, + { + MaxHealth, + Health, + Position, + Acceleration, + Velocity, + Player, + Mob, + DamagableByPlayer, + Physics, + }, + { + 100, + 100, + vector.zero, + vector.zero, + vector.zero, + } + ) +end +local function setup_goblin(entity: jecs.Entity) + jecs.bulk_insert(world, entity, + { + MaxHealth, + Health, + Position, + Acceleration, + Velocity, + Mob, + Goblin, + DamagableByPlayer, + Physics + }, + { + 200, + 200, + vector.zero, + vector.zero, + vector.zero, + } + ) +end + +local function setup_wood_spikes(entity: jecs.Entity) + jecs.bulk_insert(world, entity, + { + Position, + TouchHurts, + }, + { + vector.normalize(vector.create(math.random(), math.random(), math.random())) + } + ) +end + +type EntityKind = "player" | "goblin" | "ogre" | "big_boss_goblin" | "wood_spikes" + +local function exhaustive(x: never?) + error(x) +end + +local function entity_create(kind: EntityKind) + local new_entity = world:entity() + if kind == "player" then + setup_player(new_entity) + elseif kind == "big_boss_goblin" then + setup_goblin(new_entity) + elseif kind == "goblin" then + setup_goblin(new_entity) + elseif kind == "wood_spikes" then + setup_wood_spikes(new_entity) + elseif kind == "ogre" then + setup_goblin(new_entity) + else + exhaustive(kind) + end +end + +for i = 1, 10 do + entity_create("player") +end + +for i = 1, 100 do + entity_create("goblin") +end + +for i = 1, 1000 do + entity_create("wood_spikes") +end + +local it = 0 + +local has_physics = world:query(Position, Velocity, Acceleration):cached() +local touch_hurts_against_mobs = world:query(Position):with(TouchHurts):cached() +local damageable_mobs = world:query(Position, Health):with(DamagableByPlayer):cached() +local players = world:query(Position):with(Player):cached() +local dying_mobs = world:query(Health):cached() + +local function physics(dt: number) + for entity, pos, vel, acc in has_physics do + vel += acc * dt + pos += vel * dt + acc = vector.zero + world:set(entity, Velocity, vel) + world:set(entity, Position, vel) + world:set(entity, Acceleration, vel) + it+=1 + end +end + +local function touch_hurts_mobs() + for _, arch2 in damageable_mobs:archetypes() do + local against_positions = arch2.columns_map[Position] + local against_health = arch2.columns_map[Health] + for row2, health in against_health do + it += 1 + local against_pos = against_positions[row2] + for _, arch1 in touch_hurts_against_mobs:archetypes() do + local entity_positions = arch1.columns_map[Position] + for row, entity_pos in entity_positions do + it += 1 + if vector.magnitude(against_pos - entity_pos) < 5 then + continue + end + health -= 1 + end + end + + against_health[row2] = health + end + end +end + +local function players_damage_mobs() + for _, arch2 in damageable_mobs:archetypes() do + local against_health = arch2.columns_map[Health] + for row2, health in against_health do + it += 1 + for _, arch1 in players:archetypes() do + local entity_positions = arch1.columns_map[Position] + for row, entity_pos in entity_positions do + it += 1 + health -= 1 + end + end + + against_health[row2] = health + end + end +end + +local function mobs_die() + for entity, health in dying_mobs do + it += 1 + if health <= 0 then + world:delete(entity) + end + end +end + +local function update(dt: number, tick: number) + it = 0 + physics(dt) + + touch_hurts_mobs(dt) + + players_damage_mobs(dt) + + mobs_die(dt) + + if (tick % 10)==0 then + entity_create("goblin") + entity_create("player") + end +end + +-- 0.062 seconds for 1000 frames. +for i = 1, 1000 do + update(1/60, i) +end diff --git a/benches/smallgame.luau b/benches/smallgame.luau new file mode 100755 index 0000000..8e9ba33 --- /dev/null +++ b/benches/smallgame.luau @@ -0,0 +1,213 @@ +local gamestate = { + entities = {} :: { Entity }, + entity_top_count = 0, + entity_id_gen = 0 +} +for i = 1, 2048 do + gamestate.entities[i] = {} :: Entity +end + +type EntityKind = "player" | "goblin" | "ogre" | "big_boss_goblin" | "wood_spikes" +type EntityHandle = { + index: number, + id: number +} +type Entity = { + allocated: boolean, + handle: EntityHandle, + kind: EntityKind, + + has_physics: boolean, + damagable_by_player: boolean, + is_mob: boolean, + blocks_mobs: boolean, + + position: vector, + velocity: vector, + acceleration: vector, + hitbox: vector, + hit_cooldown_end_time: number, + health: number, + next_attack_time: number, + sprite_id: number, + current_animation_frame: number, + + update: (Entity) -> (), + draw: (Entity) -> (), + max_health: number, + +} + +local function exhaustive(x: never?) + error(x) +end + +local function entity_delete(entity: Entity) + table.clear(entity::any) +end + +local it = 0 +local it_qualified = 0 +local function setup_player(entity: Entity) + entity.kind = "player" + entity.is_mob = true + entity.damagable_by_player = true + entity.max_health = 100 + entity.health = 100 + entity.has_physics = true + entity.acceleration = vector.zero + entity.velocity = vector.zero + entity.position = vector.zero + + entity.update = function() + entity.health = entity.max_health + entity.acceleration += vector.create( + math.random(1, 5), + math.random(1, 5), + math.random(1, 5) + ) + if entity.health <= 0 then + entity_delete(entity) + end + + for _, against in gamestate.entities do + it += 1 + if not against.allocated then + continue + end + if not against.damagable_by_player then + continue + end + it_qualified += 1 + against.health -= 1 + + end + end +end +local function setup_goblin(entity: Entity) + entity.kind = "goblin" + entity.is_mob = true + entity.damagable_by_player = true + entity.max_health = 200 + entity.health = 200 + entity.has_physics = true + entity.acceleration = vector.zero + entity.velocity = vector.zero + entity.position = vector.zero + + entity.update = function() + entity.acceleration += vector.normalize(vector.create(math.random(), math.random(), math.random())) + if entity.health <= 0 then + entity_delete(entity) + end + end +end + +local function setup_wood_spikes(entity: Entity) + entity.kind = "wood_spikes" + entity.is_mob = false + entity.damagable_by_player = false + entity.position = vector.create( + math.random(1, 5), + math.random(1, 5), + math.random(1, 5) + ) + + entity.update = function() + for _, against in gamestate.entities do + it += 1 + if not against.allocated then + continue + end + if against.is_mob == false then + continue + end + if vector.magnitude(against.position - entity.position) < 5 then + continue + end + + it_qualified += 1 + against.health -= 1 + end + end +end + +local function entity_create(kind: EntityKind): Entity + local new_entity: Entity = nil::any + local new_index = 0 + for index, entity in gamestate.entities do + if entity.allocated then + continue + end + new_entity = entity + new_index = index + break + end + + assert(new_index ~= 0) + gamestate.entity_top_count += 1 + + new_entity.handle = {} :: EntityHandle + new_entity.allocated = true + gamestate.entity_id_gen += 1 + new_entity.handle.id = gamestate.entity_id_gen + new_entity.handle.index = new_index + + gamestate.entities[new_index] = new_entity + + if kind == "player" then + setup_player(new_entity) + elseif kind == "big_boss_goblin" then + setup_goblin(new_entity) + elseif kind == "goblin" then + setup_goblin(new_entity) + elseif kind == "wood_spikes" then + setup_wood_spikes(new_entity) + elseif kind == "ogre" then + setup_goblin(new_entity) + else + exhaustive(kind) + end + + return new_entity +end + +for i = 1, 10 do + entity_create("player") +end + +for i = 1, 100 do + entity_create("goblin") +end + +for i = 1, 1000 do + entity_create("wood_spikes") +end + +local function update(dt: number, tick: number) + it = 0 + it_qualified = 0 + for _, entity in gamestate.entities do + it += 1 + if not entity.allocated then + continue + end + it_qualified += 1 + entity.update(entity) + + if entity.has_physics then + entity.velocity += entity.acceleration * dt + entity.position += entity.velocity * dt + entity.acceleration = vector.zero + end + end + if (tick % 10)==0 then + entity_create("goblin") + entity_create("player") + end +end + +-- 14.839 seconds for 1000 frames +for i = 1, 1000 do + update(1/60, i) +end diff --git a/examples/luau/hooks/traits/isa.luau b/examples/luau/hooks/traits/isa.luau new file mode 100755 index 0000000..71125b1 --- /dev/null +++ b/examples/luau/hooks/traits/isa.luau @@ -0,0 +1,46 @@ +local jecs = require("@jecs") +local world = jecs.world() +local IsA = world:entity() +world:set(IsA, jecs.OnAdd, function(component, id) + local second = jecs.pair_second(world, id) + assert(second ~= component, "circular") + + local is_tag = jecs.is_tag(world, second) + world:added(component, function(entity, _, value) + if is_tag then + world:add(entity, second) + else + world:set(entity, second, value) + end + end) + + world:removed(component, function(entity) + world:remove(entity, second) + end) + + if not is_tag then + world:changed(component, function(entity, _, value) + world:set(entity, second, value) + end) + end + + local r = jecs.record(world, second) :: jecs.Record + local archetype = r.archetype + if not archetype then + return + end + local types = archetype.types + + for _, id in types do + if jecs.is_tag(world, id) then + world:add(component, id) + else + local metadata = world:get(second, id) + if not world:has(component, id) then + world:set(component, id, metadata) + end + end + end + + -- jecs.bulk_insert(world, component, ids, values) +end) diff --git a/perf.svg b/perf.svg new file mode 100755 index 0000000..18dd810 --- /dev/null +++ b/perf.svg @@ -0,0 +1,654 @@ + + + + + + + + + + + + + +Flame Graph +Reset Zoom +Search +ic + + + + + + +
Function: [:0] (86,990 usec, 100.0%); self: 0 usec
+ + + +
+ + +./benches/ecsgame.luau:1 +
Function: [./benches/ecsgame.luau:1] (59,386 usec, 68.3%); self: 900 usec
+ + + +
+ + +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1 +
Function: [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1] (27,604 usec, 31.7%); self: 27,604 usec
+ + + +
+ +update +./benches/ecsgame.luau:117 +
Function: update [./benches/ecsgame.luau:117] (55,932 usec, 64.3%); self: 48,036 usec
+ +update +update +
+ +entity_create +./benches/ecsgame.luau:83 +
Function: entity_create [./benches/ecsgame.luau:83] (1,800 usec, 2.1%); self: 0 usec
+ +e.. +entity_create +
+ +world_new +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2206 +
Function: world_new [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2206] (100 usec, 0.1%); self: 0 usec
+ + +world_new +
+ +require +
Function: require [[C]:0] (654 usec, 0.8%); self: 654 usec
+ + +require +
+ +entity_create +./benches/ecsgame.luau:83 +
Function: entity_create [./benches/ecsgame.luau:83] (4,195 usec, 4.8%); self: 100 usec
+ +entity.. +entity_create +
+ +world_query_iter_next +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1747 +
Function: world_query_iter_next [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1747] (300 usec, 0.3%); self: 300 usec
+ + +world_query_iter_next +
+ +world_set +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2399 +
Function: world_set [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2399] (400 usec, 0.5%); self: 400 usec
+ + +world_set +
+ +world_query_iter_next +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1798 +
Function: world_query_iter_next [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1798] (400 usec, 0.5%); self: 400 usec
+ + +world_query_iter_next +
+ +query_archetypes +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1163 +
Function: query_archetypes [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1163] (100 usec, 0.1%); self: 100 usec
+ + +query_archetypes +
+ +world_delete +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2920 +
Function: world_delete [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2920] (2,201 usec, 2.5%); self: 1,900 usec
+ +wo.. +world_delete +
+ +cached_query_iter +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1667 +
Function: cached_query_iter [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1667] (300 usec, 0.3%); self: 300 usec
+ + +cached_query_iter +
+ +setup_player +./benches/ecsgame.luau:20 +
Function: setup_player [./benches/ecsgame.luau:20] (100 usec, 0.1%); self: 0 usec
+ + +setup_player +
+ +world_entity +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2825 +
Function: world_entity [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2825] (400 usec, 0.5%); self: 100 usec
+ + +world_entity +
+ +setup_goblin +./benches/ecsgame.luau:42 +
Function: setup_goblin [./benches/ecsgame.luau:42] (100 usec, 0.1%); self: 0 usec
+ + +setup_goblin +
+ +setup_wood_spikes +./benches/ecsgame.luau:65 +
Function: setup_wood_spikes [./benches/ecsgame.luau:65] (1,200 usec, 1.4%); self: 200 usec
+ + +setup_wood_spikes +
+ +world_add +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2501 +
Function: world_add [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2501] (100 usec, 0.1%); self: 100 usec
+ + +world_add +
+ +setup_goblin +./benches/ecsgame.luau:42 +
Function: setup_goblin [./benches/ecsgame.luau:42] (2,563 usec, 2.9%); self: 400 usec
+ +se.. +setup_goblin +
+ +world_entity +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2825 +
Function: world_entity [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2825] (102 usec, 0.1%); self: 102 usec
+ + +world_entity +
+ +setup_player +./benches/ecsgame.luau:20 +
Function: setup_player [./benches/ecsgame.luau:20] (1,430 usec, 1.6%); self: 100 usec
+ + +setup_player +
+ +archetype_delete +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1095 +
Function: archetype_delete [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:1095] (301 usec, 0.3%); self: 301 usec
+ + +archetype_delete +
+ +ecs_bulk_insert +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080 +
Function: ecs_bulk_insert [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080] (100 usec, 0.1%); self: 100 usec
+ + +ecs_bulk_insert +
+ +entity_index_new_id +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2254 +
Function: entity_index_new_id [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2254] (300 usec, 0.3%); self: 100 usec
+ + +entity_index_new_id +
+ +ecs_bulk_insert +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080 +
Function: ecs_bulk_insert [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080] (100 usec, 0.1%); self: 100 usec
+ + +ecs_bulk_insert +
+ +ecs_bulk_insert +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080 +
Function: ecs_bulk_insert [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080] (1,000 usec, 1.1%); self: 900 usec
+ + +ecs_bulk_insert +
+ +ecs_bulk_insert +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080 +
Function: ecs_bulk_insert [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080] (2,063 usec, 2.4%); self: 2,063 usec
+ +e.. +ecs_bulk_insert +
+ +GC +
Function: GC [GC:0] (100 usec, 0.1%); self: 100 usec
+ + +GC +
+ +ecs_bulk_insert +C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080 +
Function: ecs_bulk_insert [C:/Users/Marcus/Documents/packages/jecs/jecs.luau:2080] (1,230 usec, 1.4%); self: 1,130 usec
+ + +ecs_bulk_insert +
+ +GC +
Function: GC [GC:0] (100 usec, 0.1%); self: 100 usec
+ + +GC +
+ +GC +
Function: GC [GC:0] (200 usec, 0.2%); self: 200 usec
+ + +GC +
+ +concat +
Function: concat [[C]:0] (100 usec, 0.1%); self: 0 usec
+ + +concat +
+ +concat +
Function: concat [[C]:0] (100 usec, 0.1%); self: 0 usec
+ + +concat +
+ +GC +
Function: GC [GC:0] (100 usec, 0.1%); self: 100 usec
+ + +GC +
+ +GC +
Function: GC [GC:0] (100 usec, 0.1%); self: 100 usec
+ + +GC +
+
+
+ diff --git a/perf2.svg b/perf2.svg new file mode 100755 index 0000000..ab81724 --- /dev/null +++ b/perf2.svg @@ -0,0 +1,430 @@ + + + + + + + + + + + + + +Flame Graph +Reset Zoom +Search +ic + + + + + + +
Function: [:0] (33,030,510 usec, 100.0%); self: 0 usec
+ + + +
+ + +./benches/smallgame.luau:1 +
Function: [./benches/smallgame.luau:1] (33,030,510 usec, 100.0%); self: 2,214 usec
+ + + +
+ +entity_create +./benches/smallgame.luau:120 +
Function: entity_create [./benches/smallgame.luau:120] (2,900 usec, 0.0%); self: 2,400 usec
+ + +entity_create +
+ +update +./benches/smallgame.luau:176 +
Function: update [./benches/smallgame.luau:176] (33,025,296 usec, 100.0%); self: 134,690 usec
+ +update +update +
+ + +./benches/smallgame.luau:104 +
Function: [./benches/smallgame.luau:104] (23,826,193 usec, 72.1%); self: 23,826,193 usec
+ + + +
+ +entity_create +./benches/smallgame.luau:120 +
Function: entity_create [./benches/smallgame.luau:120] (16,987 usec, 0.1%); self: 15,984 usec
+ + +entity_create +
+ + +./benches/smallgame.luau:60 +
Function: [./benches/smallgame.luau:60] (9,046,526 usec, 27.4%); self: 9,046,526 usec
+ + + +
+
+
+ diff --git a/tools/__pycache__/svg.cpython-312.pyc b/tools/__pycache__/svg.cpython-312.pyc new file mode 100755 index 0000000000000000000000000000000000000000..efe7e72151709a957ae13fb6c3668472d725f1a8 GIT binary patch literal 20032 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