From cc7daa6a069fa5b44e9e7b8e836105be461294df Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 29 Jan 2025 08:28:08 +0100 Subject: [PATCH] Cleanup codebase --- .gitignore | 1 - CHANGELOG.md | 2 +- jecs.luau | 202 +++++++++--------- test/gen.luau | 183 ----------------- test/lol.luau | 1 + test/testkit.luau | 28 ++- test/tests.luau | 335 ++++++++++-------------------- tools/perfgraph.py | 177 ++++++++++++++++ tools/svg.py | 501 +++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 923 insertions(+), 507 deletions(-) delete mode 100644 test/gen.luau create mode 100644 tools/perfgraph.py create mode 100644 tools/svg.py diff --git a/.gitignore b/.gitignore index c34903a..322caff 100644 --- a/.gitignore +++ b/.gitignore @@ -64,5 +64,4 @@ drafts/ .vitepress/dist # Luau tools -/tools profile.* diff --git a/CHANGELOG.md b/CHANGELOG.md index b3180c6..497e7d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -165,7 +165,7 @@ The format is based on [Keep a Changelog][kac], and this project adheres to - Separates ranges for components and entity IDs. - - IDs created with `world:component()` will promote array lookups rather than map lookups in the `componentIndex` which is a significant boost + - IDs created with `world:component()` will promote array lookups rather than map lookups in the `component_index` which is a significant boost - No longer caches the column pointers directly and instead the column indices which stay persistent even when data is reallocated during swap-removals - This was an issue with the iterator being invalidated when you move an entity to a different archetype. diff --git a/jecs.luau b/jecs.luau index 59b00f7..c0b4cf3 100644 --- a/jecs.luau +++ b/jecs.luau @@ -275,7 +275,7 @@ local function query_match(query, archetype: Archetype) end local function find_observers(world: World, event, component): { Observer }? - local cache = world.observerable[event] + local cache = world.observable[event] if not cache then return nil end @@ -471,7 +471,7 @@ local function world_target(world: World, entity: i53, relation: i24, index: num return nil end - local idr = world.componentIndex[ECS_PAIR(relation, EcsWildcard)] + local idr = world.component_index[ECS_PAIR(relation, EcsWildcard)] if not idr then return nil end @@ -502,8 +502,8 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean end local function id_record_ensure(world: World, id: number): IdRecord - local componentIndex = world.componentIndex - local idr: IdRecord = componentIndex[id] + local component_index = world.component_index + local idr: IdRecord = component_index[id] if not idr then local flags = ECS_ID_MASK @@ -550,7 +550,7 @@ local function id_record_ensure(world: World, id: number): IdRecord }, } - componentIndex[id] = idr + component_index[id] = idr end return idr @@ -575,8 +575,8 @@ local function archetype_append_to_records( end local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype - local archetype_id = (world.nextArchetypeId :: number) + 1 - world.nextArchetypeId = archetype_id + local archetype_id = (world.max_archetype_id :: number) + 1 + world.max_archetype_id = archetype_id local length = #id_types local columns = (table.create(length) :: any) :: { Column } @@ -632,7 +632,7 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) end end - world.archetypeIndex[ty] = archetype + world.archetype_index[ty] = archetype world.archetypes[archetype_id] = archetype return archetype @@ -652,7 +652,7 @@ local function archetype_ensure(world: World, id_types): Archetype end local ty = hash(id_types) - local archetype = world.archetypeIndex[ty] + local archetype = world.archetype_index[ty] if archetype then return archetype end @@ -814,7 +814,7 @@ local function world_add(world: World, entity: i53, id: i53): () end end - local idr = world.componentIndex[id] + local idr = world.component_index[id] local on_add = idr.hooks.on_add if on_add then @@ -831,15 +831,10 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () local from: Archetype = record.archetype local to: Archetype = archetype_traverse_add(world, id, from) - local idr = world.componentIndex[id] - local flags = idr.flags - local is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0 + local idr = world.component_index[id] local idr_hooks = idr.hooks if from == to then - if is_tag then - return - end -- If the archetypes are the same it can avoid moving the entity -- and just set the data directly. local tr = to.records[id] @@ -868,10 +863,6 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () on_add(entity) end - if is_tag then - return - end - local tr = to.records[id] local column = to.columns[tr.column] @@ -884,15 +875,15 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () end local function world_component(world: World): i53 - local componentId = (world.nextComponentId :: number) + 1 - if componentId > HI_COMPONENT_ID then + local id = (world.max_component_id :: number) + 1 + if id > HI_COMPONENT_ID then -- IDs are partitioned into ranges because component IDs are not nominal, -- so it needs to error when IDs intersect into the entity range. error("Too many components, consider using world:entity() instead to create components.") end - world.nextComponentId = componentId + world.max_component_id = id - return componentId + return id end local function world_remove(world: World, entity: i53, id: i53) @@ -909,7 +900,7 @@ local function world_remove(world: World, entity: i53, id: i53) local to = archetype_traverse_remove(world, id, from) if from and not (from == to) then - local idr = world.componentIndex[id] + local idr = world.component_index[id] local on_remove = idr.hooks.on_remove if on_remove then on_remove(entity) @@ -937,28 +928,27 @@ local function archetype_fast_delete(columns: { Column }, column_count: number, end local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) - local entityIndex = world.entity_index + local entity_index = world.entity_index + local component_index = world.component_index local columns = archetype.columns local id_types = archetype.types local entities = archetype.entities local column_count = #entities local last = #entities local move = entities[last] - local delete = entities[row] - entities[row] = move - entities[last] = nil :: any + -- We assume first that the entity is the last in the archetype + local delete = move if row ~= last then - -- TODO: should be "entity_index_sparse_get(entityIndex, move)" - local record_to_move = entity_index_try_get_any(entityIndex, move) + local record_to_move = entity_index_try_get_any(entity_index, move) if record_to_move then record_to_move.row = row end + + entities[row] = move + delete = entities[row] end - -- TODO: if last == 0 then deactivate table - - local component_index = world.componentIndex for _, id in id_types do local idr = component_index[id] local on_remove = idr.hooks.on_remove @@ -967,6 +957,8 @@ local function archetype_delete(world: World, archetype: Archetype, row: number, end end + entities[last] = nil :: any + if row == last then archetype_fast_delete_last(columns, column_count, id_types, delete) else @@ -1048,11 +1040,11 @@ local function archetype_destroy(world: World, archetype: Archetype) return end - local component_index = world.componentIndex + local component_index = world.component_index archetype_clear_edges(archetype) local archetype_id = archetype.id world.archetypes[archetype_id] = nil :: any - world.archetypeIndex[archetype.type] = nil :: any + world.archetype_index[archetype.type] = nil :: any local records = archetype.records for id in records do @@ -1096,7 +1088,7 @@ local function world_cleanup(world: World) end world.archetypes = new_archetypes - world.archetypeIndex = new_archetype_map + world.archetype_index = new_archetype_map end local world_delete: (world: World, entity: i53, destruct: boolean?) -> () @@ -1118,7 +1110,7 @@ do end local delete = entity - local component_index = world.componentIndex + local component_index = world.component_index local archetypes: Archetypes = world.archetypes local tgt = ECS_PAIR(EcsWildcard, delete) local idr_t = component_index[tgt] @@ -1135,6 +1127,8 @@ do for i = n, 1, -1 do world_delete(world, entities[i]) end + + archetype_destroy(world, idr_archetype) end else for archetype_id in idr.cache do @@ -1144,10 +1138,7 @@ do for i = n, 1, -1 do world_remove(world, entities[i], delete) end - end - for archetype_id in idr.cache do - local idr_archetype = archetypes[archetype_id] archetype_destroy(world, idr_archetype) end end @@ -1164,6 +1155,8 @@ do table.insert(children, child) end + local n = #children + for _, id in idr_t_types do if not ECS_IS_PAIR(id) then continue @@ -1174,22 +1167,30 @@ do local flags = id_record.flags local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) if flags_delete_mask ~= 0 then - for _, child in children do - -- Cascade deletions of it has Delete as component trait - world_delete(world, child, destruct) + for i = n, 1, -1 do + world_delete(world, children[i]) end break else local on_remove = id_record.hooks.on_remove local to = archetype_traverse_remove(world, id, idr_t_archetype) if on_remove then - for _, child in children do - on_remove(child) - local r = entity_index_try_get_fast(entity_index, child) :: Record - entity_move(entity_index, child, r, to) + if to then + for i = n, 1, -1 do + local child = children[i] + on_remove(children[i]) + local r = entity_index_try_get_fast(entity_index, child) :: Record + entity_move(entity_index, child, r, to) + end + else + for i = n, 1, -1 do + local child = children[i] + on_remove(child) + end end - else - for _, child in children do + elseif to then + for i = n, 1, -1 do + local child = children[i] local r = entity_index_try_get_fast(entity_index, child) :: Record entity_move(entity_index, child, r, to) end @@ -1608,14 +1609,14 @@ local function query_cached(query: QueryInner) local records: { ArchetypeRecord } local archetypes = query.compatible_archetypes - local world = query.world :: World + local world = query.world :: { observable: Observable } -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively -- because the event will be emitted for all components of that Archetype. - local observerable = world.observerable - local on_create_action = observerable[EcsOnArchetypeCreate] + local observable = world.observable :: Observable + local on_create_action = observable[EcsOnArchetypeCreate] if not on_create_action then on_create_action = {} - observerable[EcsOnArchetypeCreate] = on_create_action + observable[EcsOnArchetypeCreate] = on_create_action end local query_cache_on_create = on_create_action[A] if not query_cache_on_create then @@ -1623,10 +1624,10 @@ local function query_cached(query: QueryInner) on_create_action[A] = query_cache_on_create end - local on_delete_action = observerable[EcsOnArchetypeDelete] + local on_delete_action = observable[EcsOnArchetypeDelete] if not on_delete_action then on_delete_action = {} - observerable[EcsOnArchetypeDelete] = on_delete_action + observable[EcsOnArchetypeDelete] = on_delete_action end local query_cache_on_delete = on_delete_action[A] if not query_cache_on_delete then @@ -1920,7 +1921,7 @@ local function world_query(world: World, ...) local archetypes = world.archetypes local idr: IdRecord? - local componentIndex = world.componentIndex + local component_index = world.component_index local q = setmetatable({ ids = ids, @@ -1929,7 +1930,7 @@ local function world_query(world: World, ...) }, Query) for _, id in ids do - local map = componentIndex[id] + local map = component_index[id] if not map then return q end @@ -1972,7 +1973,7 @@ local function world_query(world: World, ...) end local function world_each(world: World, id): () -> () - local idr = world.componentIndex[id] + local idr = world.component_index[id] if not idr then return NOOP end @@ -2146,15 +2147,16 @@ function World.new() max_id = 0, } local self = setmetatable({ - archetypeIndex = {} :: { [string]: Archetype }, + archetype_index = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, - componentIndex = {} :: ComponentIndex, + component_index = {} :: ComponentIndex, entity_index = entity_index, - nextArchetypeId = 0 :: number, - nextComponentId = 0 :: number, - nextEntityId = 0 :: number, ROOT_ARCHETYPE = (nil :: any) :: Archetype, - observerable = {}, + + max_archetype_id = 0, + max_component_id = 0, + + observable = {} :: Observable, }, World) :: any self.ROOT_ARCHETYPE = archetype_create(self, {}, "") @@ -2194,6 +2196,8 @@ function World.new() return self end +export type Entity = number & { __T: T } + export type Id = | Entity | Pair, Entity> @@ -2205,29 +2209,11 @@ export type Pair = number & { __O: O, } --- type function ecs_id_t(entity) --- local ty = entity:components()[2] --- local __T = ty:readproperty(types.singleton("__T")) --- if not __T then --- return ty:readproperty(types.singleton("__jecs_pair_value")) --- end --- return __T --- end - --- type function ecs_pair_t(first, second) --- if ecs_id_t(first):is("nil") then --- return second --- else --- return first --- end --- end - type Item = (self: Query) -> (Entity, T...) -export type Entity = number & { __T: T } - type Iter = (query: Query) -> () -> (Entity, T...) + export type Query = typeof(setmetatable({}, { __iter = (nil :: any) :: Iter, })) & { @@ -2238,30 +2224,31 @@ export type Query = typeof(setmetatable({}, { cached: (self: Query) -> Query, } -type Observer = { +export type Observer = { callback: (archetype: Archetype) -> (), query: QueryInner, } +type Observable = { + [i53]: { + [i53]: { + { Observer } + } + } +} + export type World = { - archetypeIndex: { [string]: Archetype }, + archetype_index: { [string]: Archetype }, archetypes: Archetypes, - componentIndex: ComponentIndex, + component_index: ComponentIndex, entity_index: EntityIndex, ROOT_ARCHETYPE: Archetype, - nextComponentId: number, - nextEntityId: number, - nextArchetypeId: number, + max_component_id: number, + max_archetype_id: number, + + observable: any, - observerable: { - [i53]: { - [i53]: { - { query: QueryInner, callback: (Archetype) -> () } - } - } - }, -} & { --- Creates a new entity entity: (self: World) -> Entity, --- Creates a new entity located in the first 256 ids. @@ -2312,6 +2299,22 @@ export type World = { & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) } +-- type function ecs_id_t(entity) +-- local ty = entity:components()[2] +-- local __T = ty:readproperty(types.singleton("__T")) +-- if not __T then +-- return ty:readproperty(types.singleton("__jecs_pair_value")) +-- end +-- return __T +-- end + +-- type function ecs_pair_t(first, second) +-- if ecs_id_t(first):is("nil") then +-- return second +-- else +-- return first +-- end +-- end return { World = World :: { new: () -> World }, @@ -2370,4 +2373,7 @@ return { query_with = query_with, query_without = query_without, query_archetypes = query_archetypes, + query_match = query_match, + + find_observers = find_observers, } diff --git a/test/gen.luau b/test/gen.luau deleted file mode 100644 index bf13dbf..0000000 --- a/test/gen.luau +++ /dev/null @@ -1,183 +0,0 @@ -type i53 = number -type i24 = number - -type Ty = { i53 } -type ArchetypeId = number - -type Column = { any } - -type Map = { [K]: V } - -type GraphEdge = { - from: Archetype, - to: Archetype?, - prev: GraphEdge?, - next: GraphEdge?, - id: number, -} - -type GraphEdges = Map - -type GraphNode = { - add: GraphEdges, - remove: GraphEdges, - refs: GraphEdge, -} - -type ArchetypeRecord = { - count: number, - column: number, -} - -export type Archetype = { - id: number, - node: GraphNode, - types: Ty, - type: string, - entities: { number }, - columns: { Column }, - records: { ArchetypeRecord }, -} -type Record = { - archetype: Archetype, - row: number, - dense: i24, -} - -type EntityIndex = { - dense_array: Map, - sparse_array: Map, - sparse_count: number, - alive_count: number, - max_id: number, -} - -local ECS_PAIR_FLAG = 0x8 -local ECS_ID_FLAGS_MASK = 0x10 -local ECS_ENTITY_MASK = bit32.lshift(1, 24) -local ECS_GENERATION_MASK = bit32.lshift(1, 16) - --- HIGH 24 bits LOW 24 bits -local function ECS_GENERATION(e: i53): i24 - return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 -end - -local function ECS_COMBINE(source: number, target: number): i53 - return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) -end - -local function ECS_GENERATION_INC(e: i53) - if e > ECS_ENTITY_MASK then - local flags = e // ECS_ID_FLAGS_MASK - local id = flags // ECS_ENTITY_MASK - local generation = flags % ECS_GENERATION_MASK - print(generation) - return ECS_COMBINE(id, generation + 1) - end - return ECS_COMBINE(e, 1) -end -local function ECS_ENTITY_T_LO(e: i53): i24 - return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e -end - -local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record? - local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] - - if not r or r.dense == 0 then - return nil - end - - return r -end - -local function entity_index_try_get(entity_index: EntityIndex, entity: number): Record? - local r = entity_index_try_get_any(entity_index, entity) - if r then - local r_dense = r.dense - if r_dense > entity_index.alive_count then - return nil - end - if entity_index.dense_array[r_dense] ~= entity then - return nil - end - end - return r -end - -local function entity_index_get_alive(entity_index: EntityIndex, entity: number): number - local r = entity_index_try_get_any(entity_index, entity) - if r then - return entity_index.dense_array[r.dense] - end - return 0 -end - -local function entity_index_remove(entity_index: EntityIndex, entity: number) - local r = entity_index_try_get(entity_index, entity) - if not r then - return - end - local dense_array = entity_index.dense_array - local index_of_deleted_entity = r.dense - local last_entity_alive_at_index = entity_index.alive_count - entity_index.alive_count -= 1 - - local last_alive_entity = dense_array[last_entity_alive_at_index] - local r_swap = entity_index_try_get_any(entity_index, last_alive_entity) :: Record - r_swap.dense = index_of_deleted_entity - r.archetype = nil :: any - r.row = nil :: any - r.dense = last_entity_alive_at_index - - dense_array[index_of_deleted_entity] = last_alive_entity - dense_array[last_entity_alive_at_index] = ECS_GENERATION_INC(entity) -end - -local function entity_index_new_id(entity_index: EntityIndex): i53 - local dense_array = entity_index.dense_array - if entity_index.alive_count ~= #dense_array then - entity_index.alive_count += 1 - local id = dense_array[entity_index.alive_count] - return id - end - - entity_index.max_id += 1 - local id = entity_index.max_id - entity_index.alive_count += 1 - - dense_array[entity_index.alive_count] = id - entity_index.sparse_array[id] = { - dense = entity_index.alive_count, - } :: Record - - return id -end - -local function entity_index_is_alive(entity_index: EntityIndex, entity: number) - return entity_index_try_get(entity_index, entity) ~= nil -end - -local eidx = { - alive_count = 0, - max_id = 0, - sparse_array = {} :: { Record }, - sparse_count = 0, - dense_array = {} :: { i53 }, -} -local e1v0 = entity_index_new_id(eidx, "e1v0") -local e2v0 = entity_index_new_id(eidx, "e2v0") -local e3v0 = entity_index_new_id(eidx, "e3v0") -local e4v0 = entity_index_new_id(eidx, "e4v0") -local e5v0 = entity_index_new_id(eidx, "e5v0") - -local e6v0 = entity_index_new_id(eidx) -entity_index_remove(eidx, e6v0) -local e6v1 = entity_index_new_id(eidx) -entity_index_remove(eidx, e6v1) -local e6v2 = entity_index_new_id(eidx) -print(ECS_ENTITY_T_LO(e6v2), ECS_GENERATION(e6v2)) - -print("-----") -local e2 = ECS_GENERATION_INC(ECS_GENERATION_INC(269)) -print("-----") -print(ECS_ENTITY_T_LO(e2), ECS_GENERATION(e2)) diff --git a/test/lol.luau b/test/lol.luau index cbf50dd..0be2c57 100644 --- a/test/lol.luau +++ b/test/lol.luau @@ -123,6 +123,7 @@ local function remove(entity) r.dense = last_entity_alive_at_index dense[index_of_deleted_entity] = last_alive_entity dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity) + print("*dellocated", pe(id)) end local function alive(e) diff --git a/test/testkit.luau b/test/testkit.luau index d52484b..d362114 100644 --- a/test/testkit.luau +++ b/test/testkit.luau @@ -180,6 +180,24 @@ local function CASE(name: string) table.insert(test.cases, case) end +local function CHECK_EXPECT_ERR(fn, ...) + assert(test, "no active test") + local case = test.case + if not case then + CASE("") + case = test.case + end + assert(case, "no active case") + if case.result ~= FAIL then + local ok, err = pcall(fn, ...) + case.result = if ok then FAIL else PASS + if skip then + case.result = SKIPPED + end + case.line = debug.info(stack and stack + 1 or 2, "l") + end +end + local function CHECK(value: T, stack: number?): T? assert(test, "no active test") @@ -509,7 +527,15 @@ end return { test = function() - return TEST, CASE, CHECK, FINISH, SKIP, FOCUS + return { + TEST = TEST, + CASE = CASE, + CHECK = CHECK, + FINISH = FINISH, + SKIP = SKIP, + FOCUS = FOCUS, + CHECK_EXPECT_ERR = CHECK_EXPECT_ERR + } end, benchmark = function() diff --git a/test/tests.luau b/test/tests.luau index d7bec16..8425b70 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -15,7 +15,11 @@ local entity_index_is_alive = jecs.entity_index_is_alive local ChildOf = jecs.ChildOf local world_new = jecs.World.new -local TEST, CASE, CHECK, FINISH, SKIP, FOCUS = testkit.test() +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 N = 2 ^ 8 @@ -68,19 +72,6 @@ local function name(world, e) end TEST("archetype", function() - local archetype_append_to_records = jecs.archetype_append_to_records - local id_record_ensure = jecs.id_record_ensure - local archetype_create = jecs.archetype_create - local archetype_ensure = jecs.archetype_ensure - local find_insert = jecs.find_insert - local find_archetype_with = jecs.find_archetype_with - local find_archetype_without = jecs.find_archetype_without - local archetype_init_edge = jecs.archetype_init_edge - local archetype_ensure_edge = jecs.archetype_ensure_edge - local init_edge_for_add = jecs.init_edge_for_add - local init_edge_for_remove = jecs.init_edge_for_remove - local create_edge_for_add = jecs.create_edge_for_add - local create_edge_for_remove = jecs.create_edge_for_remove local archetype_traverse_add = jecs.archetype_traverse_add local archetype_traverse_remove = jecs.archetype_traverse_remove @@ -90,7 +81,7 @@ TEST("archetype", function() local c2 = world:component() local c3 = world:component() - local a1 = archetype_traverse_add(world, c1, nil) + local a1 = archetype_traverse_add(world, c1, nil :: any) local a2 = archetype_traverse_remove(world, c1, a1) CHECK(root.add[c1].to == a1) CHECK(root == a2) @@ -116,11 +107,11 @@ TEST("world:cleanup()", function() world:set(e3, B, true) world:set(e3, C, true) - local archetypeIndex = world.archetypeIndex + local archetype_index = world.archetype_index - CHECK(#archetypeIndex["1"].entities == 1) - CHECK(#archetypeIndex["1_2"].entities == 1) - CHECK(#archetypeIndex["1_2_3"].entities == 1) + CHECK(#archetype_index["1"].entities == 1) + CHECK(#archetype_index["1_2"].entities == 1) + CHECK(#archetype_index["1_2_3"].entities == 1) world:delete(e1) world:delete(e2) @@ -128,25 +119,25 @@ TEST("world:cleanup()", function() world:cleanup() - archetypeIndex = world.archetypeIndex + archetype_index = world.archetype_index - CHECK((archetypeIndex["1"] :: jecs.Archetype?) == nil) - CHECK((archetypeIndex["1_2"] :: jecs.Archetype?) == nil) - CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil) + CHECK((archetype_index["1"] :: jecs.Archetype?) == nil) + CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil) + CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil) local e4 = world:entity() world:set(e4, A, true) - CHECK(#archetypeIndex["1"].entities == 1) - CHECK((archetypeIndex["1_2"] :: jecs.Archetype?) == nil) - CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil) + CHECK(#archetype_index["1"].entities == 1) + CHECK((archetype_index["1_2"] :: jecs.Archetype?) == nil) + CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil) world:set(e4, B, true) - CHECK(#archetypeIndex["1"].entities == 0) - CHECK(#archetypeIndex["1_2"].entities == 1) - CHECK((archetypeIndex["1_2_3"] :: jecs.Archetype?) == nil) + CHECK(#archetype_index["1"].entities == 0) + CHECK(#archetype_index["1_2"].entities == 1) + CHECK((archetype_index["1_2_3"] :: jecs.Archetype?) == nil) world:set(e4, C, true) - CHECK(#archetypeIndex["1"].entities == 0) - CHECK(#archetypeIndex["1_2"].entities == 0) - CHECK(#archetypeIndex["1_2_3"].entities == 1) + CHECK(#archetype_index["1"].entities == 0) + CHECK(#archetype_index["1_2"].entities == 0) + CHECK(#archetype_index["1_2_3"].entities == 1) end) TEST("world:entity()", function() @@ -163,14 +154,14 @@ TEST("world:entity()", function() do CASE("generations") local world = jecs.World.new() - local e = world:entity() + local e = world:entity() :: number CHECK(ECS_ID(e) == 1 + jecs.Rest :: number) CHECK(ECS_GENERATION(e) == 0) -- 0 e = ECS_GENERATION_INC(e) CHECK(ECS_GENERATION(e) == 1) -- 1 end - do CASE("pairs") + do CASE "pairs" local world = jecs.World.new() local _e = world:entity() local e2 = world:entity() @@ -182,8 +173,8 @@ TEST("world:entity()", function() local p = pair(e2, e3) CHECK(IS_PAIR(p) == true) - CHECK(ecs_pair_first(world, p) == e2) - CHECK(ecs_pair_second(world, p) == e3) + CHECK(ecs_pair_first(world, p) == e2 :: number) + CHECK(ecs_pair_second(world, p) == e3 :: number) world:delete(e2) local e2v2 = world:entity() @@ -199,7 +190,7 @@ TEST("world:entity()", function() local e1 = world:entity() world:delete(e1) local e2 = world:entity() - CHECK(ECS_ID(e2) == e) + CHECK(ECS_ID(e2) == e :: number) CHECK(ECS_GENERATION(e2) == 2) CHECK(world:contains(e2)) CHECK(not world:contains(e1)) @@ -224,8 +215,7 @@ TEST("world:entity()", function() end) TEST("world:set()", function() - do - CASE("archetype move") + do CASE "archetype move" do local world = jecs.World.new() @@ -255,26 +245,11 @@ TEST("world:set()", function() -- Should have tuple of fields to the next archetype and set the component data CHECK(d.tuple(e, 1, 2)) -- Should have moved the data from the old archetype - CHECK(world.archetypeIndex[oldArchetype].columns[_1][oldRow] == nil) + CHECK(world.archetype_index[oldArchetype].columns[_1][oldRow] == nil) end end - do - CASE("arbitrary order") - local world = jecs.World.new() - - local Health = world:entity() - local Poison = world:component() - - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) - - CHECK(world:get(id, Poison) == 5) - end - - do - CASE("pairs") + do CASE "pairs" local world = jecs.World.new() local C1 = world:component() @@ -287,7 +262,10 @@ TEST("world:set()", function() world:set(e, pair(C1, C2), true) world:set(e, pair(C1, T1), true) world:set(e, pair(T1, C1), true) - world:set(e, pair(T1, T2), true) + + CHECK_EXPECT_ERR(function() + world:set(e, pair(T1, T2), true :: any) + end) CHECK(world:get(e, pair(C1, C2))) CHECK(world:get(e, pair(C1, T1))) @@ -296,7 +274,9 @@ TEST("world:set()", function() local e2 = world:entity() - world:set(e2, pair(jecs.ChildOf, e), true) + CHECK_EXPECT_ERR(function() + world:set(e2, pair(jecs.ChildOf, e), true :: any) + end) CHECK(not world:get(e2, pair(jecs.ChildOf, e))) end end) @@ -385,21 +365,21 @@ TEST("world:query()", function() i=2 end CHECK(i == 2) - for _, e in q do + for _, e in q :: any do i=3 end CHECK(i == 3) - for _, e in q do + for _, e in q :: any do i=4 end CHECK(i == 4) CHECK(#q:archetypes() == 1) - CHECK(not table.find(q:archetypes(), world.archetypes[table.concat({Foo, Bar, Baz}, "_")])) + CHECK(not table.find(q:archetypes(), world.archetype_index[table.concat({Foo, Bar, Baz}, "_")])) world:delete(Foo) CHECK(#q:archetypes() == 0) end - do CASE("multiple iter") + do CASE "multiple iter" local world = jecs.World.new() local A = world:component() local B = world:component() @@ -416,18 +396,21 @@ TEST("world:query()", function() end CHECK(counter == 2) end - do - CASE("tag") + do CASE "tag" local world = jecs.World.new() local A = world:entity() local e = world:entity() - world:set(e, A, "test") - for id, a in world:query(A) do + CHECK_EXPECT_ERR(function() + world:set(e, A, "test" :: any) + end) + local count = 0 + for id, a in world:query(A) :: any do + count += 1 CHECK(a == nil) end + CHECK(count == 1) end - do - CASE("pairs") + do CASE "pairs" local world = jecs.World.new() local C1 = world:component() @@ -440,9 +423,11 @@ TEST("world:query()", function() world:set(e, pair(C1, C2), true) world:set(e, pair(C1, T1), true) world:set(e, pair(T1, C1), true) - world:set(e, pair(T1, T2), true) + CHECK_EXPECT_ERR(function() + world:set(e, pair(T1, T2), true :: any) + end) - for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) do + for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) :: any do CHECK(a == true) CHECK(b == true) CHECK(c == true) @@ -467,7 +452,7 @@ TEST("world:query()", function() entities[i] = id end - for id in world:query(A) do + for id in world:query(A) :: any do table.remove(entities, CHECK(table.find(entities, id))) end @@ -491,10 +476,10 @@ TEST("world:query()", function() local i = 0 local j = 0 - for _ in q do + for _ in q :: any do i += 1 end - for _ in q do + for _ in q :: any do j += 1 end CHECK(i == 2) @@ -518,7 +503,7 @@ TEST("world:query()", function() world:set(e2, B, 457) local counter = 0 - for _ in world:query(B, C) do + for _ in world:query(B, C) :: any do counter += 1 end CHECK(counter == 0) @@ -538,7 +523,7 @@ TEST("world:query()", function() world:set(e, id, 13 ^ i) end - for entity, a, b, c, d, e, f, g, h, i in world:query(unpack(components)) do + for entity, a, b, c, d, e, f, g, h, i in world:query(unpack(components)) :: any do CHECK(a == 13 ^ 1) CHECK(b == 13 ^ 2) CHECK(c == 13 ^ 3) @@ -567,11 +552,11 @@ TEST("world:query()", function() local it = world:query(A):iter() - local e, data = it() + local e: number, data = it() while e do - if e == eA then + if e == eA :: number then CHECK(data) - elseif e == eAB then + elseif e == eAB :: number then CHECK(data) else CHECK(false) @@ -582,8 +567,7 @@ TEST("world:query()", function() CHECK(true) end - do - CASE("should query all matching entities when irrelevant component is removed") + do CASE "should query all matching entities when irrelevant component is removed" local world = jecs.World.new() local A = world:component() local B = world:component() @@ -604,7 +588,7 @@ TEST("world:query()", function() end local added = 0 - for id in world:query(A) do + for id in world:query(A) :: any do added += 1 table.remove(entities, CHECK(table.find(entities, id))) end @@ -630,7 +614,7 @@ TEST("world:query()", function() end end - for id in world:query(A):without(B) do + for id in world:query(A):without(B) :: any do table.remove(entities, CHECK(table.find(entities, id))) end @@ -645,7 +629,7 @@ TEST("world:query()", function() local bob = world:entity() world:set(bob, pair(Eats, Apples), true) - for e, bool in world:query(pair(Eats, Apples)) do + for e, bool in world:query(pair(Eats, Apples)) :: any do CHECK(e == bob) CHECK(bool) end @@ -661,11 +645,11 @@ TEST("world:query()", function() world:set(bob, pair(Eats, Apples), "bob eats apples") local w = jecs.Wildcard - for e, data in world:query(pair(Eats, w)) do + for e, data in world:query(pair(Eats, w)) :: any do CHECK(e == bob) CHECK(data == "bob eats apples") end - for e, data in world:query(pair(w, Apples)) do + for e, data in world:query(pair(w, Apples)) :: any do CHECK(e == bob) CHECK(data == "bob eats apples") end @@ -685,7 +669,7 @@ TEST("world:query()", function() local w = jecs.Wildcard local count = 0 - for e, data in world:query(pair(Eats, w)) do + for e, data in world:query(pair(Eats, w)) :: any do count += 1 if e == bob then CHECK(data == "bob eats apples") @@ -697,32 +681,30 @@ TEST("world:query()", function() CHECK(count == 2) count = 0 - for e, data in world:query(pair(w, Apples)) do + for e, data in world:query(pair(w, Apples)) :: any do count += 1 CHECK(data == "bob eats apples") end CHECK(count == 1) end - do - CASE("should only relate alive entities") - SKIP() + do CASE "should only relate alive entities" local world = jecs.World.new() local Eats = world:entity() - local Apples = world:entity() - local Oranges = world:entity() + local Apples = world:component() + local Oranges = world:component() local bob = world:entity() local alice = world:entity() world:set(bob, Apples, "apples") world:set(bob, pair(Eats, Apples), "bob eats apples") - world:set(alice, pair(Eats, Oranges), "alice eats oranges") + world:set(alice, pair(Eats, Oranges) :: Entity, "alice eats oranges") world:delete(Apples) local Wildcard = jecs.Wildcard local count = 0 - for _, data in world:query(pair(Wildcard, Apples)) do + for _, data in world:query(pair(Wildcard, Apples)) :: any do count += 1 end @@ -730,6 +712,7 @@ TEST("world:query()", function() CHECK(count == 0) CHECK(world:get(bob, pair(Eats, Apples)) == nil) + end do @@ -759,10 +742,10 @@ TEST("world:query()", function() world:set(bob, Name, "bob") world:add(sara, pair(ChildOf, alice)) world:set(sara, Name, "sara") - CHECK(world:parent(bob) == alice) -- O(1) + CHECK(world:parent(bob) :: number == alice :: number) -- O(1) local count = 0 - for _, name in world:query(Name, pair(ChildOf, alice)) do + for _, name in world:query(Name, pair(ChildOf, alice)) :: any do count += 1 end CHECK(count == 2) @@ -781,7 +764,7 @@ TEST("world:query()", function() world:add(e2, B) local count = 0 - for id in world:query(A) do + for id in world:query(A) :: any do world:clear(id) count += 1 end @@ -804,7 +787,7 @@ TEST("world:query()", function() world:add(e2, B) local count = 0 - for id in world:query(A) do + for id in world:query(A) :: any do world:add(id, B) count += 1 @@ -825,7 +808,7 @@ TEST("world:query()", function() world:add(e2, A) world:add(e2, B) - for id in world:query(A) do + for id in world:query(A) :: any do local e = world:entity() world:add(e, A) world:add(e, B) @@ -847,7 +830,7 @@ TEST("world:query()", function() world:add(helloBob, Bob) local withoutCount = 0 - for _ in world:query(pair(Hello, Bob)):without(Bob) do + for _ in world:query(pair(Hello, Bob)):without(Bob) :: any do withoutCount += 1 end @@ -862,7 +845,7 @@ TEST("world:query()", function() local _1, _2, _3 = world:component(), world:component(), world:component() local counter = 0 - for e, a, b in world:query(_1, _2):without(_3) do + for e, a, b in world:query(_1, _2):without(_3) :: any do counter += 1 end CHECK(counter == 0) @@ -876,9 +859,9 @@ TEST("world:each", function() local B = world:component() local C = world:component() + local e3 = world:entity() local e1 = world:entity() local e2 = world:entity() - local e3 = world:entity() world:set(e1, A, true) @@ -889,8 +872,8 @@ TEST("world:each", function() world:set(e3, B, true) world:set(e3, C, true) - for entity in world:each(A) do - if entity == e1 or entity == e2 or entity == e3 then + for entity: number in world:each(A) do + if entity == e1 :: number or entity == e2 :: number or entity == e3 :: number then CHECK(true) continue end @@ -906,16 +889,16 @@ TEST("world:children", function() local e1 = world:entity() world:set(e1, C, true) - local e2 = world:entity() + local e2 = world:entity() :: number world:add(e2, T) world:add(e2, pair(ChildOf, e1)) - local e3 = world:entity() + local e3 = world:entity() :: number world:add(e3, pair(ChildOf, e1)) local count = 0 - for entity in world:children(e1) do + for entity: number in world:children(e1) do count += 1 if entity == e2 or entity == e3 then CHECK(true) @@ -966,7 +949,7 @@ TEST("world:clear()", function() world:add(e, A) world:add(e1, A) - local archetype = world.archetypeIndex["1"] + local archetype = world.archetype_index["1"] local archetype_entities = archetype.entities local _e = e :: number @@ -1050,7 +1033,9 @@ TEST("world:component()", function() local e = world:entity() world:set(e, A, "test") world:add(e, B) - world:set(e, C, 11) + CHECK_EXPECT_ERR(function() + world:set(e, C, 11 :: any) + end) CHECK(world:has(e, A)) CHECK(world:get(e, A) == "test") @@ -1113,10 +1098,14 @@ TEST("world:delete", function() local id = world:entity() world:set(id, Poison, 5) - world:set(id, Health, 50) + CHECK_EXPECT_ERR(function() + world:set(id, Health, 50 :: any) + end) local id1 = world:entity() world:set(id1, Poison, 500) - world:set(id1, Health, 50) + CHECK_EXPECT_ERR(function() + world:set(id1, Health, 50 :: any) + end) CHECK(world:has(id, Poison, Health)) CHECK(world:has(id1, Poison, Health)) @@ -1355,7 +1344,7 @@ TEST("Hooks", function() do -- basic local world = jecs.World.new() - local A = world:component() + local A = world:component() :: Entity local e1 = world:entity() world:add(e1, A) world:set(A, jecs.OnRemove, function(entity) @@ -1387,9 +1376,9 @@ TEST("Hooks", function() end) TEST("change tracking", function() - CASE "#1" do + do CASE "#1" local world = world_new() - local Foo = world:component() + local Foo = world:component() :: Entity local Previous = jecs.Rest local q1 = world @@ -1403,14 +1392,14 @@ TEST("change tracking", function() world:set(e2, Foo, 2) local i = 0 - for e, new in q1 do + for e, new in q1 :: any do i += 1 world:set(e, pair(Previous, Foo), new) end CHECK(i == 2) local j = 0 - for e, new in q1 do + for e, new in q1 :: any do j += 1 world:set(e, pair(Previous, Foo), new) end @@ -1418,9 +1407,9 @@ TEST("change tracking", function() CHECK(j == 0) end - CASE "#2" do + do CASE "#2" local world = world_new() - local component = world:component() + local component = world:component() :: Entity local tag = world:entity() local previous = jecs.Rest @@ -1431,14 +1420,14 @@ TEST("change tracking", function() world:set(testEntity, component, 10) local i = 0 - for entity, number in q1 do + for entity, number in q1 :: any do i += 1 world:add(testEntity, tag) end CHECK(i == 1) - for e, n in q1 do + for e, n in q1 :: any do world:set(e, pair(previous, component), n) end end @@ -1477,107 +1466,7 @@ TEST("repro", function() updateCooldowns(1.5) end - do CASE "#2" - local world = jecs.World.new() - - export type Iterator = () -> (Entity, T?, T?) - export type Destructor = () -> () - - -- Helpers - - type ValuesMap = { [Entity]: T? } - type ChangeSet = { [Entity]: true? } - type ChangeSets = { [ChangeSet]: true? } - type ChangeSetsCache = { - Added: ChangeSets, - Changed: ChangeSets, - Removed: ChangeSets, - } - - local cachedChangeSets = {} - local function getChangeSets(component): ChangeSetsCache - if cachedChangeSets[component] == nil then - local changeSetsAdded: ChangeSets = {} - local changeSetsChanged: ChangeSets = {} - local changeSetsRemoved: ChangeSets = {} - world:set(component, jecs.OnAdd, function(id) - for set in changeSetsAdded do - set[id] = true - end - end) - world:set(component, jecs.OnSet, function(id) - for set in changeSetsChanged do - set[id] = true - end - end) - world:set(component, jecs.OnRemove, function(id) - for set in changeSetsRemoved do - set[id] = true - end - end) - cachedChangeSets[component] = { - Added = changeSetsAdded, - Changed = changeSetsChanged, - Removed = changeSetsRemoved, - } - end - return cachedChangeSets[component] - end - - local function ChangeTracker(component: jecs.Id): (Iterator, Destructor) - local values: ValuesMap = {} - local changeSet: ChangeSet = {} - - for id in world:query(component) do - changeSet[id] = true - end - - local changeSets = getChangeSets(component) - changeSets.Added[changeSet] = true - changeSets.Changed[changeSet] = true - changeSets.Removed[changeSet] = true - - local id: jecs.Id? = nil - local iter: Iterator = function() - id = next(changeSet) - if id then - changeSet[id] = nil - local old: T? = values[id] - local new: T? = world:get(id, component) - if old ~= nil and new == nil then - -- Old value but no new value = removed - values[id] = nil - else - -- Old+new value or just new value = new becomes old - values[id] = new - end - return id, old, new - end - return nil :: any, nil, nil - end - - local destroy: Destructor = function() - changeSets.Added[changeSet] = nil - changeSets.Changed[changeSet] = nil - changeSets.Removed[changeSet] = nil - end - - return iter, destroy - end - - local Transform = world:component() - local iter, destroy = ChangeTracker(Transform) - - local e1 = world:entity() - world:set(e1, Transform, { 1, 1 }) - local counter = 0 - for _ in iter do - counter += 1 - end - CHECK(counter == 1) - end - - do CASE "#3" -- ISSUE #171 + do CASE "#2" -- ISSUE #171 local world = world_new() local component1 = world:component() local tag1 = world:entity() @@ -1663,7 +1552,7 @@ end) TEST("world:delete() invokes OnRemove hook", function() do CASE "#1" local world = world_new() - + local A = world:entity() local entity = world:entity() @@ -1696,7 +1585,7 @@ TEST("world:delete() invokes OnRemove hook", function() world:add(entity, A) world:add(entity, pair(Relation, B)) - + world:delete(B) CHECK(called) diff --git a/tools/perfgraph.py b/tools/perfgraph.py new file mode 100644 index 0000000..a6bc1dc --- /dev/null +++ b/tools/perfgraph.py @@ -0,0 +1,177 @@ +#!/usr/bin/python3 +# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +# Given a profile dump, this tool generates a flame graph based on the stacks listed in the profile +# The result of analysis is a .svg file which can be viewed in a browser + +import svg +import argparse +import json + +argumentParser = argparse.ArgumentParser(description='Generate flamegraph SVG from Luau sampling profiler dumps') +argumentParser.add_argument('source_file', type=open) +argumentParser.add_argument('--json', dest='useJson',action='store_const',const=1,default=0,help='Parse source_file as JSON') + +class Node(svg.Node): + def __init__(self): + svg.Node.__init__(self) + self.function = "" + self.source = "" + self.line = 0 + self.ticks = 0 + + def text(self): + return self.function + + def title(self): + if self.line > 0: + return "{}\n{}:{}".format(self.function, self.source, self.line) + else: + return self.function + + def details(self, root): + return "Function: {} [{}:{}] ({:,} usec, {:.1%}); self: {:,} usec".format(self.function, self.source, self.line, self.width, self.width / root.width, self.ticks) + + +def nodeFromCallstackListFile(source_file): + dump = source_file.readlines() + root = Node() + + for l in dump: + ticks, stack = l.strip().split(" ", 1) + node = root + + for f in reversed(stack.split(";")): + source, function, line = f.split(",") + + child = node.child(f) + child.function = function + child.source = source + child.line = int(line) if len(line) > 0 else 0 + + node = child + + node.ticks += int(ticks) + + return root + + +def getDuration(nodes, nid): + node = nodes[nid - 1] + total = node['TotalDuration'] + + if 'NodeIds' in node: + for cid in node['NodeIds']: + total -= nodes[cid - 1]['TotalDuration'] + + return total + +def getFunctionKey(fn): + source = fn['Source'] if 'Source' in fn else '' + name = fn['Name'] if 'Name' in fn else '' + line = str(fn['Line']) if 'Line' in fn else '-1' + + return source + "," + name + "," + line + +def recursivelyBuildNodeTree(nodes, functions, parent, fid, nid): + ninfo = nodes[nid - 1] + finfo = functions[fid - 1] + + child = parent.child(getFunctionKey(finfo)) + child.source = finfo['Source'] if 'Source' in finfo else '' + child.function = finfo['Name'] if 'Name' in finfo else '' + child.line = int(finfo['Line']) if 'Line' in finfo and finfo['Line'] > 0 else 0 + + child.ticks = getDuration(nodes, nid) + + if 'FunctionIds' in ninfo: + assert(len(ninfo['FunctionIds']) == len(ninfo['NodeIds'])) + + for i in range(0, len(ninfo['FunctionIds'])): + recursivelyBuildNodeTree(nodes, functions, child, ninfo['FunctionIds'][i], ninfo['NodeIds'][i]) + + return + +def nodeFromJSONV2(dump): + assert(dump['Version'] == 2) + + nodes = dump['Nodes'] + functions = dump['Functions'] + categories = dump['Categories'] + + root = Node() + + for category in categories: + nid = category['NodeId'] + node = nodes[nid - 1] + name = category['Name'] + + child = root.child(name) + child.function = name + child.ticks = getDuration(nodes, nid) + + if 'FunctionIds' in node: + assert(len(node['FunctionIds']) == len(node['NodeIds'])) + + for i in range(0, len(node['FunctionIds'])): + recursivelyBuildNodeTree(nodes, functions, child, node['FunctionIds'][i], node['NodeIds'][i]) + + return root + +def getDurationV1(obj): + total = obj['TotalDuration'] + + if 'Children' in obj: + for key, obj in obj['Children'].items(): + total -= obj['TotalDuration'] + + return total + + +def nodeFromJSONObject(node, key, obj): + source, function, line = key.split(",") + + node.function = function + node.source = source + node.line = int(line) if len(line) > 0 else 0 + + node.ticks = getDurationV1(obj) + + if 'Children' in obj: + for key, obj in obj['Children'].items(): + nodeFromJSONObject(node.child(key), key, obj) + + return node + +def nodeFromJSONV1(dump): + assert(dump['Version'] == 1) + root = Node() + + if 'Children' in dump: + for key, obj in dump['Children'].items(): + nodeFromJSONObject(root.child(key), key, obj) + + return root + +def nodeFromJSONFile(source_file): + dump = json.load(source_file) + + if dump['Version'] == 2: + return nodeFromJSONV2(dump) + elif dump['Version'] == 1: + return nodeFromJSONV1(dump) + + return Node() + + +arguments = argumentParser.parse_args() + +if arguments.useJson: + root = nodeFromJSONFile(arguments.source_file) +else: + root = nodeFromCallstackListFile(arguments.source_file) + + + +svg.layout(root, lambda n: n.ticks) +svg.display(root, "Flame Graph", "hot", flip = True) diff --git a/tools/svg.py b/tools/svg.py new file mode 100644 index 0000000..385db91 --- /dev/null +++ b/tools/svg.py @@ -0,0 +1,501 @@ +# This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +class Node: + def __init__(self): + self.name = "" + self.children = {} + # computed + self.depth = 0 + self.width = 0 + self.offset = 0 + + def child(self, name): + node = self.children.get(name) + if not node: + node = self.__class__() + node.name = name + self.children[name] = node + return node + + def subtree(self): + result = [self] + offset = 0 + + while offset < len(result): + p = result[offset] + offset += 1 + for c in p.children.values(): + result.append(c) + + return result + +def escape(s): + return s.replace("&", "&").replace("<", "<").replace(">", ">") + +def layout(root, widthcb): + for n in reversed(root.subtree()): + # propagate width to the parent + n.width = widthcb(n) + for c in n.children.values(): + n.width += c.width + + # compute offset from parent for every child in width order (layout order) + offset = 0 + for c in sorted(n.children.values(), key = lambda x: x.width, reverse = True): + c.offset = offset + offset += c.width + + for n in root.subtree(): + for c in n.children.values(): + c.depth = n.depth + 1 + c.offset += n.offset + +# svg template (stolen from framegraph.pl) +template = r""" + + + + + + + + + + + + +$title +Reset Zoom +Search +ic + + + +""" + +def namehash(s): + # FNV-1a + hval = 0x811c9dc5 + for ch in s: + hval = hval ^ ord(ch) + hval = hval * 0x01000193 + hval = hval % (2 ** 32) + return (hval % 31337) / 31337.0 + +def display(root, title, colors, flip = False): + if colors == "cold": + gradient_start = "#eef2ee" + gradient_end = "#e0ffe0" + else: + gradient_start = "#eeeeee" + gradient_end = "#eeeeb0" + + maxdepth = 0 + for n in root.subtree(): + maxdepth = max(maxdepth, n.depth) + + svgheight = maxdepth * 16 + 3 * 16 + 2 * 16 + + print(template + .replace("$title", title) + .replace("$gradient-start", gradient_start) + .replace("$gradient-end", gradient_end) + .replace("$height", str(svgheight)) + .replace("$status", str((svgheight - 16 + 3 if flip else 3 * 16 - 3))) + .replace("$flip", str(int(flip))) + ) + + framewidth = 1200 - 20 + + def pixels(x): + return float(x) / root.width * framewidth if root.width > 0 else 0 + + for n in root.subtree(): + if pixels(n.width) < 0.1: + continue + + x = 10 + pixels(n.offset) + y = (maxdepth - 1 - n.depth if flip else n.depth) * 16 + 3 * 16 + width = pixels(n.width) + height = 15 + + if colors == "cold": + fillr = 0 + fillg = int(190 + 50 * namehash(n.name)) + fillb = int(210 * namehash(n.name[::-1])) + else: + fillr = int(205 + 50 * namehash(n.name)) + fillg = int(230 * namehash(n.name[::-1])) + fillb = int(55 * namehash(n.name[::-2])) + + fill = "rgb({},{},{})".format(fillr, fillg, fillb) + chars = width / (12 * 0.59) + + text = n.text() + + if chars >= 3: + if chars < len(text): + text = text[:int(chars-2)] + ".." + else: + text = "" + + print("") + print("{}".format(escape(n.title()))) + print("
{}
".format(escape(n.details(root)))) + print("".format(x, y, width, height, fill)) + print("{}".format(x + 3, y + 10.5, escape(text))) + print("{}".format(escape(n.text()))) + print("
") + + print("
\n
\n")