Cleanup codebase
Some checks failed
analysis / Run Luau Analyze (push) Has been cancelled
deploy-docs / build (push) Has been cancelled
publish-npm / publish (push) Has been cancelled
unit-testing / Run Luau Tests (push) Has been cancelled
deploy-docs / Deploy (push) Has been cancelled

This commit is contained in:
Ukendio 2025-01-29 08:28:08 +01:00
parent a39fc8d0a2
commit cc7daa6a06
9 changed files with 923 additions and 507 deletions

1
.gitignore vendored
View file

@ -64,5 +64,4 @@ drafts/
.vitepress/dist
# Luau tools
/tools
profile.*

View file

@ -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.

196
jecs.luau
View file

@ -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)
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 _, child in children do
for i = n, 1, -1 do
local child = children[i]
on_remove(child)
end
end
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<T = nil> = number & { __T: T }
export type Id<T = nil> =
| Entity<T>
| Pair<Entity<T>, Entity>
@ -2205,29 +2209,11 @@ export type Pair<P, O> = 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<T...> = (self: Query<T...>) -> (Entity, T...)
export type Entity<T = nil> = number & { __T: T }
type Iter<T...> = (query: Query<T...>) -> () -> (Entity, T...)
export type Query<T...> = typeof(setmetatable({}, {
__iter = (nil :: any) :: Iter<T...>,
})) & {
@ -2238,30 +2224,31 @@ export type Query<T...> = typeof(setmetatable({}, {
cached: (self: Query<T...>) -> Query<T...>,
}
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 = {
& (<A, B, C, D, E, F, G>(World, Id<A>, Id<B>, Id<C>, Id<D>, Id<E>, Id<F>, Id<G>) -> Query<A, B, C, D, E, F, G>)
& (<A, B, C, D, E, F, G, H>(World, Id<A>, Id<B>, Id<C>, Id<D>, Id<E>, Id<F>, Id<G>, Id<H>, ...Id<any>) -> Query<A, B, C, D, E, F, G, H>)
}
-- 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,
}

View file

@ -1,183 +0,0 @@
type i53 = number
type i24 = number
type Ty = { i53 }
type ArchetypeId = number
type Column = { any }
type Map<K, V> = { [K]: V }
type GraphEdge = {
from: Archetype,
to: Archetype?,
prev: GraphEdge?,
next: GraphEdge?,
id: number,
}
type GraphEdges = Map<i53, GraphEdge>
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<i24, i53>,
sparse_array: Map<i53, Record>,
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))

View file

@ -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)

View file

@ -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<T>(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()

View file

@ -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<string>, "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<boolean>
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<number>
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<number>
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<T> = () -> (Entity, T?, T?)
export type Destructor = () -> ()
-- Helpers
type ValuesMap<T> = { [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<T>(component: jecs.Id): (Iterator<T>, Destructor)
local values: ValuesMap<T> = {}
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<T> = 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()

177
tools/perfgraph.py Normal file
View file

@ -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)

501
tools/svg.py Normal file
View file

@ -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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
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"""<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" width="1200" height="$height" onload="init(evt)" viewBox="0 0 1200 $height" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Flame graph stack visualization. See https://github.com/brendangregg/FlameGraph for latest version, and http://www.brendangregg.com/flamegraphs.html for examples. -->
<defs>
<linearGradient id="background" y1="0" y2="1" x1="0" x2="0" >
<stop stop-color="$gradient-start" offset="5%" />
<stop stop-color="$gradient-end" offset="95%" />
</linearGradient>
</defs>
<style type="text/css">
text { font-family:Verdana; font-size:12px; fill:rgb(0,0,0); }
#search, #ignorecase { opacity:0.1; cursor:pointer; }
#search:hover, #search.show, #ignorecase:hover, #ignorecase.show { opacity:1; }
#subtitle { text-anchor:middle; font-color:rgb(160,160,160); }
#title { text-anchor:middle; font-size:17px}
#unzoom { cursor:pointer; }
#frames > *:hover { stroke:black; stroke-width:0.5; cursor:pointer; }
.hide { display:none; }
.parent { opacity:0.5; }
</style>
<script type="text/ecmascript">
<![CDATA[
"use strict";
var details, searchbtn, unzoombtn, matchedtxt, svg, searching, currentSearchTerm, ignorecase, ignorecaseBtn;
function init(evt) {
details = document.getElementById("details").firstChild;
searchbtn = document.getElementById("search");
ignorecaseBtn = document.getElementById("ignorecase");
unzoombtn = document.getElementById("unzoom");
matchedtxt = document.getElementById("matched");
svg = document.getElementsByTagName("svg")[0];
searching = 0;
currentSearchTerm = null;
}
window.addEventListener("click", function(e) {
var target = find_group(e.target);
if (target) {
if (target.nodeName == "a") {
if (e.ctrlKey === false) return;
e.preventDefault();
}
if (target.classList.contains("parent")) unzoom();
zoom(target);
}
else if (e.target.id == "unzoom") unzoom();
else if (e.target.id == "search") search_prompt();
else if (e.target.id == "ignorecase") toggle_ignorecase();
}, false)
// mouse-over for info
// show
window.addEventListener("mouseover", function(e) {
var target = find_group(e.target);
if (target) details.nodeValue = g_to_text(target);
}, false)
// clear
window.addEventListener("mouseout", function(e) {
var target = find_group(e.target);
if (target) details.nodeValue = ' ';
}, false)
// ctrl-F for search
window.addEventListener("keydown",function (e) {
if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
e.preventDefault();
search_prompt();
}
}, false)
// ctrl-I to toggle case-sensitive search
window.addEventListener("keydown",function (e) {
if (e.ctrlKey && e.keyCode === 73) {
e.preventDefault();
toggle_ignorecase();
}
}, false)
// functions
function find_child(node, selector) {
var children = node.querySelectorAll(selector);
if (children.length) return children[0];
return;
}
function find_group(node) {
var parent = node.parentElement;
if (!parent) return;
if (parent.id == "frames") return node;
return find_group(parent);
}
function orig_save(e, attr, val) {
if (e.attributes["_orig_" + attr] != undefined) return;
if (e.attributes[attr] == undefined) return;
if (val == undefined) val = e.attributes[attr].value;
e.setAttribute("_orig_" + attr, val);
}
function orig_load(e, attr) {
if (e.attributes["_orig_"+attr] == undefined) return;
e.attributes[attr].value = e.attributes["_orig_" + attr].value;
e.removeAttribute("_orig_"+attr);
}
function g_to_text(e) {
var text = find_child(e, "details").firstChild.nodeValue;
return (text)
}
function g_to_func(e) {
var child = find_child(e, "rawtext");
return child ? child.textContent : null;
}
function update_text(e) {
var r = find_child(e, "rect");
var t = find_child(e, "text");
var w = parseFloat(r.attributes.width.value) -3;
var txt = find_child(e, "rawtext").textContent.replace(/\([^(]*\)$/,"");
t.attributes.x.value = parseFloat(r.attributes.x.value) + 3;
// Smaller than this size won't fit anything
if (w < 2 * 12 * 0.59) {
t.textContent = "";
return;
}
t.textContent = txt;
// Fit in full text width
if (/^ *$/.test(txt) || t.getSubStringLength(0, txt.length) < w)
return;
for (var x = txt.length - 2; x > 0; x--) {
if (t.getSubStringLength(0, x + 2) <= w) {
t.textContent = txt.substring(0, x) + "..";
return;
}
}
t.textContent = "";
}
// zoom
function zoom_reset(e) {
if (e.attributes != undefined) {
orig_load(e, "x");
orig_load(e, "width");
}
if (e.childNodes == undefined) return;
for (var i = 0, c = e.childNodes; i < c.length; i++) {
zoom_reset(c[i]);
}
}
function zoom_child(e, x, ratio) {
if (e.attributes != undefined) {
if (e.attributes.x != undefined) {
orig_save(e, "x");
e.attributes.x.value = (parseFloat(e.attributes.x.value) - x - 10) * ratio + 10;
if (e.tagName == "text")
e.attributes.x.value = find_child(e.parentNode, "rect[x]").attributes.x.value + 3;
}
if (e.attributes.width != undefined) {
orig_save(e, "width");
e.attributes.width.value = parseFloat(e.attributes.width.value) * ratio;
}
}
if (e.childNodes == undefined) return;
for (var i = 0, c = e.childNodes; i < c.length; i++) {
zoom_child(c[i], x - 10, ratio);
}
}
function zoom_parent(e) {
if (e.attributes) {
if (e.attributes.x != undefined) {
orig_save(e, "x");
e.attributes.x.value = 10;
}
if (e.attributes.width != undefined) {
orig_save(e, "width");
e.attributes.width.value = parseInt(svg.width.baseVal.value) - (10 * 2);
}
}
if (e.childNodes == undefined) return;
for (var i = 0, c = e.childNodes; i < c.length; i++) {
zoom_parent(c[i]);
}
}
function zoom(node) {
var attr = find_child(node, "rect").attributes;
var width = parseFloat(attr.width.value);
var xmin = parseFloat(attr.x.value);
var xmax = parseFloat(xmin + width);
var ymin = parseFloat(attr.y.value);
var ratio = (svg.width.baseVal.value - 2 * 10) / width;
// XXX: Workaround for JavaScript float issues (fix me)
var fudge = 0.0001;
unzoombtn.classList.remove("hide");
var el = document.getElementById("frames").children;
for (var i = 0; i < el.length; i++) {
var e = el[i];
var a = find_child(e, "rect").attributes;
var ex = parseFloat(a.x.value);
var ew = parseFloat(a.width.value);
var upstack;
// Is it an ancestor
if ($flip == 1) {
upstack = parseFloat(a.y.value) > ymin;
} else {
upstack = parseFloat(a.y.value) < ymin;
}
if (upstack) {
// Direct ancestor
if (ex <= xmin && (ex+ew+fudge) >= xmax) {
e.classList.add("parent");
zoom_parent(e);
update_text(e);
}
// not in current path
else
e.classList.add("hide");
}
// Children maybe
else {
// no common path
if (ex < xmin || ex + fudge >= xmax) {
e.classList.add("hide");
}
else {
zoom_child(e, xmin, ratio);
update_text(e);
}
}
}
search();
}
function unzoom() {
unzoombtn.classList.add("hide");
var el = document.getElementById("frames").children;
for(var i = 0; i < el.length; i++) {
el[i].classList.remove("parent");
el[i].classList.remove("hide");
zoom_reset(el[i]);
update_text(el[i]);
}
search();
}
// search
function toggle_ignorecase() {
ignorecase = !ignorecase;
if (ignorecase) {
ignorecaseBtn.classList.add("show");
} else {
ignorecaseBtn.classList.remove("show");
}
reset_search();
search();
}
function reset_search() {
var el = document.querySelectorAll("#frames rect");
for (var i = 0; i < el.length; i++) {
orig_load(el[i], "fill")
}
}
function search_prompt() {
if (!searching) {
var term = prompt("Enter a search term (regexp " +
"allowed, eg: ^ext4_)"
+ (ignorecase ? ", ignoring case" : "")
+ "\nPress Ctrl-i to toggle case sensitivity", "");
if (term != null) {
currentSearchTerm = term;
search();
}
} else {
reset_search();
searching = 0;
currentSearchTerm = null;
searchbtn.classList.remove("show");
searchbtn.firstChild.nodeValue = "Search"
matchedtxt.classList.add("hide");
matchedtxt.firstChild.nodeValue = ""
}
}
function search(term) {
if (currentSearchTerm === null) return;
var term = currentSearchTerm;
var re = new RegExp(term, ignorecase ? 'i' : '');
var el = document.getElementById("frames").children;
var matches = new Object();
var maxwidth = 0;
for (var i = 0; i < el.length; i++) {
var e = el[i];
var func = g_to_func(e);
var rect = find_child(e, "rect");
if (func == null || rect == null)
continue;
// Save max width. Only works as we have a root frame
var w = parseFloat(rect.attributes.width.value);
if (w > maxwidth)
maxwidth = w;
if (func.match(re)) {
// highlight
var x = parseFloat(rect.attributes.x.value);
orig_save(rect, "fill");
rect.attributes.fill.value = "rgb(230,0,230)";
// remember matches
if (matches[x] == undefined) {
matches[x] = w;
} else {
if (w > matches[x]) {
// overwrite with parent
matches[x] = w;
}
}
searching = 1;
}
}
if (!searching)
return;
searchbtn.classList.add("show");
searchbtn.firstChild.nodeValue = "Reset Search";
// calculate percent matched, excluding vertical overlap
var count = 0;
var lastx = -1;
var lastw = 0;
var keys = Array();
for (k in matches) {
if (matches.hasOwnProperty(k))
keys.push(k);
}
// sort the matched frames by their x location
// ascending, then width descending
keys.sort(function(a, b){
return a - b;
});
// Step through frames saving only the biggest bottom-up frames
// thanks to the sort order. This relies on the tree property
// where children are always smaller than their parents.
var fudge = 0.0001; // JavaScript floating point
for (var k in keys) {
var x = parseFloat(keys[k]);
var w = matches[keys[k]];
if (x >= lastx + lastw - fudge) {
count += w;
lastx = x;
lastw = w;
}
}
// display matched percent
matchedtxt.classList.remove("hide");
var pct = 100 * count / maxwidth;
if (pct != 100) pct = pct.toFixed(1)
matchedtxt.firstChild.nodeValue = "Matched: " + pct + "%";
}
]]>
</script>
<rect x="0.0" y="0" width="1200.0" height="$height.0" fill="url(#background)" />
<text id="title" x="600.00" y="24" >$title</text>
<text id="unzoom" x="10.00" y="24" class="hide">Reset Zoom</text>
<text id="search" x="1090.00" y="24" >Search</text>
<text id="ignorecase" x="1174.00" y="24" >ic</text>
<text id="matched" x="1090.00" y="$status" > </text>
<text id="details" x="10.00" y="$status" > </text>
<g id="frames">
"""
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("<g>")
print("<title>{}</title>".format(escape(n.title())))
print("<details>{}</details>".format(escape(n.details(root))))
print("<rect x='{}' y='{}' width='{}' height='{}' fill='{}' rx='2' ry='2' />".format(x, y, width, height, fill))
print("<text x='{}' y='{}'>{}</text>".format(x + 3, y + 10.5, escape(text)))
print("<rawtext>{}</rawtext>".format(escape(n.text())))
print("</g>")
print("</g>\n</svg>\n")