diff --git a/.gitattributes b/.gitattributes
index 05cde82..fdc9e36 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,2 @@
-*.luau text eol=lf
-*.html linguist-vendored
+*.luau text eol=lf
+*.html linguist-vendored
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 2e52705..349788c 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,15 +1,15 @@
-## Brief Description of your Changes.
-
-Describe what you did here. Additionally, you should link any relevant issues within this section. If there is no corresponding issue, you should include relevant information (repro steps, motivation, etc) here.
-
-## Impact of your Changes
-
-What implications will this have on the project? Will there be altered behavior or performance with this change?
-
-## Tests Performed
-
-What have you done to ensure this change has the least possible impact on the project?
-
-## Additional Comments
-
+## Brief Description of your Changes.
+
+Describe what you did here. Additionally, you should link any relevant issues within this section. If there is no corresponding issue, you should include relevant information (repro steps, motivation, etc) here.
+
+## Impact of your Changes
+
+What implications will this have on the project? Will there be altered behavior or performance with this change?
+
+## Tests Performed
+
+What have you done to ensure this change has the least possible impact on the project?
+
+## Additional Comments
+
Anything else you feel is relevant.
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 6861d7d..4d762af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,73 +1,73 @@
-# Compiled Lua sources
-luac.out
-
-# luarocks build files
-*.src.rock
-*.zip
-*.tar.gz
-*.rbxm
-
-# Object files
-*.o
-*.os
-*.ko
-*.obj
-*.elf
-
-# Precompiled Headers
-*.gch
-*.pch
-
-# Libraries
-*.lib
-*.a
-*.la
-*.lo
-*.def
-*.exp
-
-# Shared objects (inc. Windows DLLs)
-*.dll
-*.so
-*.so.*
-*.dylib
-
-# Executables
-*.exe
-*.out
-*.app
-*.i*86
-*.x86_64
-*.hex
-
-# Wally files
-DevPackages
-Packages
-wally.lock
-WallyPatches
-
-# Typescript
-/node_modules
-/include
-
-# Misc
-roblox.toml
-sourcemap.json
-drafts/
-
-# Cached Vitepress (docs)
-
-/docs/.vitepress/cache
-/docs/.vitepress/dist
-
-.vitepress/cache
-.vitepress/dist
-
-# Luau tools
-profile.*
-
-# Patch files
-
-*.patch
-
-genhtml.perl
+# Compiled Lua sources
+luac.out
+
+# luarocks build files
+*.src.rock
+*.zip
+*.tar.gz
+*.rbxm
+
+# Object files
+*.o
+*.os
+*.ko
+*.obj
+*.elf
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Libraries
+*.lib
+*.a
+*.la
+*.lo
+*.def
+*.exp
+
+# Shared objects (inc. Windows DLLs)
+*.dll
+*.so
+*.so.*
+*.dylib
+
+# Executables
+*.exe
+*.out
+*.app
+*.i*86
+*.x86_64
+*.hex
+
+# Wally files
+DevPackages
+Packages
+wally.lock
+WallyPatches
+
+# Typescript
+/node_modules
+/include
+
+# Misc
+roblox.toml
+sourcemap.json
+drafts/
+
+# Cached Vitepress (docs)
+
+/docs/.vitepress/cache
+/docs/.vitepress/dist
+
+.vitepress/cache
+.vitepress/dist
+
+# Luau tools
+profile.*
+
+# Patch files
+
+*.patch
+
+genhtml.perl
diff --git a/.luaurc b/.luaurc
index f856eba..1d36832 100644
--- a/.luaurc
+++ b/.luaurc
@@ -1,10 +1,10 @@
-{
- "aliases": {
- "jecs": "jecs",
- "testkit": "tools/testkit",
- "mirror": "mirror",
- "tools": "tools",
- "addons": "addons"
- },
- "languageMode": "strict"
-}
+{
+ "aliases": {
+ "jecs": "jecs",
+ "testkit": "tools/testkit",
+ "mirror": "mirror",
+ "tools": "tools",
+ "addons": "addons"
+ },
+ "languageMode": "strict"
+}
diff --git a/.stylua.toml b/.stylua.toml
index 0c02788..554fc2f 100644
--- a/.stylua.toml
+++ b/.stylua.toml
@@ -1,9 +1,9 @@
-syntax = "All"
-column_width = 120
-line_endings = "Unix"
-indent_type = "Tabs"
-indent_width = 4
-quote_style = "AutoPreferDouble"
-call_parentheses = "Always"
-space_after_function_names = "Never"
-collapse_simple_statement = "Never"
+syntax = "All"
+column_width = 120
+line_endings = "Unix"
+indent_type = "Tabs"
+indent_width = 4
+quote_style = "AutoPreferDouble"
+call_parentheses = "Always"
+space_after_function_names = "Never"
+collapse_simple_statement = "Never"
diff --git a/assets/jecs_darkmode.svg b/assets/jecs_darkmode.svg
index 23a466e..f64b173 100644
--- a/assets/jecs_darkmode.svg
+++ b/assets/jecs_darkmode.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/assets/jecs_lightmode.svg b/assets/jecs_lightmode.svg
index cc93360..dbcd08c 100644
--- a/assets/jecs_lightmode.svg
+++ b/assets/jecs_lightmode.svg
@@ -1,6 +1,6 @@
-
+
diff --git a/docs/api/jecs.md b/docs/api/jecs.md
index 794239f..3fb1c1c 100644
--- a/docs/api/jecs.md
+++ b/docs/api/jecs.md
@@ -1,50 +1,50 @@
-# Jecs
-
-Jecs. Just an Entity Component System.
-
-# Properties
-
-## World
-```luau
-jecs.World: World
-```
-A world is a container of all ECS data. Games can have multiple worlds but component IDs may conflict between worlds. Ensure to register the same component IDs in the same order for each world.
-
-## Wildcard
-```luau
-jecs.Wildcard: Entity
-```
-Builtin component type. This ID is used for wildcard queries.
-
-## Component
-```luau
-jecs.Component: Entity
-```
-Builtin component type. Every ID created with [world:component()](world.md#component()) has this type added to it. This is meant for querying every component ID.
-
-## ChildOf
-```luau
-jecs.ChildOf: Entity
-```
-Builtin component type. This ID is for creating parent-child hierarchies.
-
-## Rest
-```luau
-jecs.Rest: Entity
-```
-
-# Functions
-
-## pair()
-```luau
-function jecs.pair(
- first: Entity, -- The first element of the pair, referred to as the relationship of the relationship pair.
- object: Entity, -- The second element of the pair, referred to as the target of the relationship pair.
-): number -- Returns the ID with those two elements
-
-```
-::: info
-
-While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
-
-:::
+# Jecs
+
+Jecs. Just an Entity Component System.
+
+# Properties
+
+## World
+```luau
+jecs.World: World
+```
+A world is a container of all ECS data. Games can have multiple worlds but component IDs may conflict between worlds. Ensure to register the same component IDs in the same order for each world.
+
+## Wildcard
+```luau
+jecs.Wildcard: Entity
+```
+Builtin component type. This ID is used for wildcard queries.
+
+## Component
+```luau
+jecs.Component: Entity
+```
+Builtin component type. Every ID created with [world:component()](world.md#component()) has this type added to it. This is meant for querying every component ID.
+
+## ChildOf
+```luau
+jecs.ChildOf: Entity
+```
+Builtin component type. This ID is for creating parent-child hierarchies.
+
+## Rest
+```luau
+jecs.Rest: Entity
+```
+
+# Functions
+
+## pair()
+```luau
+function jecs.pair(
+ first: Entity, -- The first element of the pair, referred to as the relationship of the relationship pair.
+ object: Entity, -- The second element of the pair, referred to as the target of the relationship pair.
+): number -- Returns the ID with those two elements
+
+```
+::: info
+
+While relationship pairs can be used as components and have data associated with an ID, they cannot be used as entities. Meaning you cannot add components to a pair as the source of a binding.
+
+:::
diff --git a/docs/learn/contributing/guidelines.md b/docs/learn/contributing/guidelines.md
index 9cb71d4..d8dfda6 100644
--- a/docs/learn/contributing/guidelines.md
+++ b/docs/learn/contributing/guidelines.md
@@ -1,21 +1,21 @@
-# Contribution Guidelines
-
-Whether you found an issue, or want to make a change to jecs, we'd love to hear back from the community on what features you want or bugs you've run into.
-
-There's a few different ways you can go about this.
-
-## Creating an Issue
-
-This is what you should be filing if you have a bug you want to report.
-
-[Click here](https://github.com/Ukendio/jecs/issues/new/choose) to file a bug report. We have a few templates ready for the most common issue types.
-
-Additionally, see the [Submitting Issues](../contributing/issues) page for more information.
-
-## Creating a Pull Request
-
-This is what you should be filing if you have a change you want to merge into the main project.
-
-[Click here](https://github.com/Ukendio/jecs/compare) to select the branch you want to merge from.
-
-Additionally, see the [Submitting Pull Requests](../contributing/pull-requests) page for more information.
+# Contribution Guidelines
+
+Whether you found an issue, or want to make a change to jecs, we'd love to hear back from the community on what features you want or bugs you've run into.
+
+There's a few different ways you can go about this.
+
+## Creating an Issue
+
+This is what you should be filing if you have a bug you want to report.
+
+[Click here](https://github.com/Ukendio/jecs/issues/new/choose) to file a bug report. We have a few templates ready for the most common issue types.
+
+Additionally, see the [Submitting Issues](../contributing/issues) page for more information.
+
+## Creating a Pull Request
+
+This is what you should be filing if you have a change you want to merge into the main project.
+
+[Click here](https://github.com/Ukendio/jecs/compare) to select the branch you want to merge from.
+
+Additionally, see the [Submitting Pull Requests](../contributing/pull-requests) page for more information.
diff --git a/docs/learn/contributing/issues.md b/docs/learn/contributing/issues.md
index 982273a..58a9543 100644
--- a/docs/learn/contributing/issues.md
+++ b/docs/learn/contributing/issues.md
@@ -1,24 +1,24 @@
-# Submitting Issues
-
-When you're submitting an issue, generally they fall into a few categories:
-
-## Bug
-
-We need some information to figure out what's going wrong. At a minimum, you need to tell us:
-
- (1) What's supposed to happen
-
- (2) What actually happened
-
- (3) Steps to reproduce
-
-
-Stack traces and other useful information that you find make a bug report more likely to be fixed.
-
-Consult the template for a bug report if you don't know or have questions about how to format this.
-
-## Documentation
-
-Depending on how you go about it, this can be done as a [Pull Request](../contributing/pull-requests) instead of an issue. Generally, we need to know what was wrong, what you changed, and how it improved the documentation if it isn't obvious.
-
-We just need to know what's wrong. You should fill out a [PR](../contributing/pull-requests) if you know what should be there instead.
+# Submitting Issues
+
+When you're submitting an issue, generally they fall into a few categories:
+
+## Bug
+
+We need some information to figure out what's going wrong. At a minimum, you need to tell us:
+
+ (1) What's supposed to happen
+
+ (2) What actually happened
+
+ (3) Steps to reproduce
+
+
+Stack traces and other useful information that you find make a bug report more likely to be fixed.
+
+Consult the template for a bug report if you don't know or have questions about how to format this.
+
+## Documentation
+
+Depending on how you go about it, this can be done as a [Pull Request](../contributing/pull-requests) instead of an issue. Generally, we need to know what was wrong, what you changed, and how it improved the documentation if it isn't obvious.
+
+We just need to know what's wrong. You should fill out a [PR](../contributing/pull-requests) if you know what should be there instead.
diff --git a/docs/learn/contributing/pull-requests.md b/docs/learn/contributing/pull-requests.md
index ea57692..0f15a46 100644
--- a/docs/learn/contributing/pull-requests.md
+++ b/docs/learn/contributing/pull-requests.md
@@ -1,77 +1,77 @@
-# Submitting Pull Requests
-
-When submitting a Pull Request, there's a few reasons to do so:
-
-
-## Documentation
-
-If there's something to change with the documentation, you should follow a similar format to this example:
-
-An example of an appropriate typo-fixing PR would be:
-
->**Brief Description of your Changes**
->
->I fixed a couple of typos found in the /contributing/issues.md file.
->
->**Impact of your Changes**
->
->- Documentation is more clear and readable for the users.
->
->**Tests Performed**
->
->Ran `vitepress dev docs` and verified it was built successfully.
->
->**Additional Comments**
->
->[At Discretion]
-
-## Change in Behavior
-
-An example of an appropriate PR that adds a new feature would be:
-
->
->**Brief Description of your Changes**
->
->I added `jecs.best_function`, which gives everyone who uses the module an immediate boost in concurrent player counts. (this is a joke)
->
->**Impact of your Changes**
->
->- jecs functionality is extended to better fit the needs of the community [explain why].
->
->**Tests Performed**
->
->Added a few test cases to ensure the function runs as expected [link to changes].
->
->**Additional Comments**
->
->[At Discretion]
-
-## Addons
-
-If you made something you think should be included into the [resources page](../../resources), let us know!
-
-We have tons of examples of libraries and other tools which can be used in conjunction with jecs on this page.
-
-One example of a PR that would be accepted is:
-
->**Brief Description of your Changes**
->
->I added `jecs observers` to the addons page.
->
->**Impact of your Changes**
->
->- jecs observers are a different and important way of handling queries which benefit the users of jecs by [explain why your tool benefits users here]
->
->- [talk about why you went with this design instead of maybe an alternative]
->
->**Tests Performed**
->
-> I used this tool in conjunction with jecs and ensured it works as expected.
->
-> [If you wrote unit tests for your tool, mention it here.]
->
->**Additional Comments**
->
->[At Discretion]
-
-Keep in mind the list on the addons page is *not* exhaustive. If you came up with a tool that doesn't fit into any of the categories listed, we still want to hear from you!
+# Submitting Pull Requests
+
+When submitting a Pull Request, there's a few reasons to do so:
+
+
+## Documentation
+
+If there's something to change with the documentation, you should follow a similar format to this example:
+
+An example of an appropriate typo-fixing PR would be:
+
+>**Brief Description of your Changes**
+>
+>I fixed a couple of typos found in the /contributing/issues.md file.
+>
+>**Impact of your Changes**
+>
+>- Documentation is more clear and readable for the users.
+>
+>**Tests Performed**
+>
+>Ran `vitepress dev docs` and verified it was built successfully.
+>
+>**Additional Comments**
+>
+>[At Discretion]
+
+## Change in Behavior
+
+An example of an appropriate PR that adds a new feature would be:
+
+>
+>**Brief Description of your Changes**
+>
+>I added `jecs.best_function`, which gives everyone who uses the module an immediate boost in concurrent player counts. (this is a joke)
+>
+>**Impact of your Changes**
+>
+>- jecs functionality is extended to better fit the needs of the community [explain why].
+>
+>**Tests Performed**
+>
+>Added a few test cases to ensure the function runs as expected [link to changes].
+>
+>**Additional Comments**
+>
+>[At Discretion]
+
+## Addons
+
+If you made something you think should be included into the [resources page](../../resources), let us know!
+
+We have tons of examples of libraries and other tools which can be used in conjunction with jecs on this page.
+
+One example of a PR that would be accepted is:
+
+>**Brief Description of your Changes**
+>
+>I added `jecs observers` to the addons page.
+>
+>**Impact of your Changes**
+>
+>- jecs observers are a different and important way of handling queries which benefit the users of jecs by [explain why your tool benefits users here]
+>
+>- [talk about why you went with this design instead of maybe an alternative]
+>
+>**Tests Performed**
+>
+> I used this tool in conjunction with jecs and ensured it works as expected.
+>
+> [If you wrote unit tests for your tool, mention it here.]
+>
+>**Additional Comments**
+>
+>[At Discretion]
+
+Keep in mind the list on the addons page is *not* exhaustive. If you came up with a tool that doesn't fit into any of the categories listed, we still want to hear from you!
diff --git a/tools/perfgraph.py b/tools/perfgraph.py
index a6bc1dc..1f6ecc2 100644
--- a/tools/perfgraph.py
+++ b/tools/perfgraph.py
@@ -1,177 +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)
+#!/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/read_lcov.py b/tools/read_lcov.py
index cee7733..b93592e 100644
--- a/tools/read_lcov.py
+++ b/tools/read_lcov.py
@@ -1,153 +1,153 @@
-import os
-
-LCOV_FILE = "coverage.out"
-OUTPUT_DIR = "coverage"
-
-os.makedirs(OUTPUT_DIR, exist_ok=True)
-
-def parse_lcov(content):
- """Parses LCOV data from a single string."""
- files = {}
- current_file = None
-
- for line in content.splitlines():
- if line.startswith("SF:"):
- current_file = line[3:].strip()
- files[current_file] = {"coverage": {}, "functions": []}
- elif line.startswith("DA:") and current_file:
- parts = line[3:].split(",")
- line_num = int(parts[0])
- execution_count = int(parts[1])
- files[current_file]["coverage"][line_num] = execution_count
- elif line.startswith("FN:") and current_file:
- parts = line[3:].split(",")
- line_num = int(parts[0])
- function_name = parts[1].strip()
- files[current_file]["functions"].append({"name": function_name, "line": line_num, "hits": 0})
- elif line.startswith("FNDA:") and current_file:
- parts = line[5:].split(",")
- hit_count = int(parts[0])
- function_name = parts[1].strip()
- for func in files[current_file]["functions"]:
- if func["name"] == function_name:
- func["hits"] = hit_count
- break
-
- return files
-
-def read_source_file(filepath):
- """Reads source file content if available."""
- if not os.path.exists(filepath):
- return []
-
- with open(filepath, "r", encoding="utf-8") as f:
- return f.readlines()
-
-def generate_file_html(filepath, coverage_data, functions_data):
- """Generates an HTML file for a specific source file."""
- filename = os.path.basename(filepath)
- source_code = read_source_file(filepath)
- html_path = os.path.join(OUTPUT_DIR, f"{filename}.html")
-
- total_hits = sum(func["hits"] for func in functions_data)
- max_hits = max((func["hits"] for func in functions_data), default=0)
-
- total_functions = len(functions_data)
- covered_functions = sum(1 for func in functions_data if func["hits"] > 0)
- function_coverage_percent = (covered_functions / total_functions * 100) if total_functions > 0 else 0
-
- lines = [
- "
",
- '',
- '',
- "",
- f'
{filename} Coverage
',
- f'
Total Execution Hits: {total_hits}
',
- f'
Function Coverage Overview: {function_coverage_percent:.2f}%
',
-
- '',
-
- '
',
- '
Function Coverage:
Function
Hits
'
- ]
-
- longest_name = max((len(func["name"]) for func in functions_data), default=0)
-
- for func in functions_data:
- hit_color = "red" if func["hits"] == 0 else "green"
- lines.append(
- f'
{func["name"]}
'
- f'
{func["hits"]}
'
- )
-
- lines.append('
') # Close collapsible div
-
- lines.append('
Source Code:
Line
Hits
Code
')
-
- for i, line in enumerate(source_code, start=1):
- stripped_line = line.strip()
- class_name = "text-muted"
- if not stripped_line or stripped_line.startswith("end") or stripped_line.startswith("--"):
- count_display = "N/A"
- lines.append(f'
")
-
- with open(os.path.join(OUTPUT_DIR, "index.html"), "w", encoding="utf-8") as f:
- f.write("\n".join(index_html))
-
-with open(LCOV_FILE, "r", encoding="utf-8") as f:
- lcov_content = f.read()
-
-files_data = parse_lcov(lcov_content)
-
-for file_path, data in files_data.items():
- generate_file_html(file_path, data["coverage"], data["functions"])
-
-generate_index(files_data)
-
-print(f"Coverage report generated in {OUTPUT_DIR}/index.html")
+import os
+
+LCOV_FILE = "coverage.out"
+OUTPUT_DIR = "coverage"
+
+os.makedirs(OUTPUT_DIR, exist_ok=True)
+
+def parse_lcov(content):
+ """Parses LCOV data from a single string."""
+ files = {}
+ current_file = None
+
+ for line in content.splitlines():
+ if line.startswith("SF:"):
+ current_file = line[3:].strip()
+ files[current_file] = {"coverage": {}, "functions": []}
+ elif line.startswith("DA:") and current_file:
+ parts = line[3:].split(",")
+ line_num = int(parts[0])
+ execution_count = int(parts[1])
+ files[current_file]["coverage"][line_num] = execution_count
+ elif line.startswith("FN:") and current_file:
+ parts = line[3:].split(",")
+ line_num = int(parts[0])
+ function_name = parts[1].strip()
+ files[current_file]["functions"].append({"name": function_name, "line": line_num, "hits": 0})
+ elif line.startswith("FNDA:") and current_file:
+ parts = line[5:].split(",")
+ hit_count = int(parts[0])
+ function_name = parts[1].strip()
+ for func in files[current_file]["functions"]:
+ if func["name"] == function_name:
+ func["hits"] = hit_count
+ break
+
+ return files
+
+def read_source_file(filepath):
+ """Reads source file content if available."""
+ if not os.path.exists(filepath):
+ return []
+
+ with open(filepath, "r", encoding="utf-8") as f:
+ return f.readlines()
+
+def generate_file_html(filepath, coverage_data, functions_data):
+ """Generates an HTML file for a specific source file."""
+ filename = os.path.basename(filepath)
+ source_code = read_source_file(filepath)
+ html_path = os.path.join(OUTPUT_DIR, f"{filename}.html")
+
+ total_hits = sum(func["hits"] for func in functions_data)
+ max_hits = max((func["hits"] for func in functions_data), default=0)
+
+ total_functions = len(functions_data)
+ covered_functions = sum(1 for func in functions_data if func["hits"] > 0)
+ function_coverage_percent = (covered_functions / total_functions * 100) if total_functions > 0 else 0
+
+ lines = [
+ "",
+ '',
+ '',
+ "",
+ f'
{filename} Coverage
',
+ f'
Total Execution Hits: {total_hits}
',
+ f'
Function Coverage Overview: {function_coverage_percent:.2f}%
',
+
+ '',
+
+ '
',
+ '
Function Coverage:
Function
Hits
'
+ ]
+
+ longest_name = max((len(func["name"]) for func in functions_data), default=0)
+
+ for func in functions_data:
+ hit_color = "red" if func["hits"] == 0 else "green"
+ lines.append(
+ f'
{func["name"]}
'
+ f'
{func["hits"]}
'
+ )
+
+ lines.append('
') # Close collapsible div
+
+ lines.append('
Source Code:
Line
Hits
Code
')
+
+ for i, line in enumerate(source_code, start=1):
+ stripped_line = line.strip()
+ class_name = "text-muted"
+ if not stripped_line or stripped_line.startswith("end") or stripped_line.startswith("--"):
+ count_display = "N/A"
+ lines.append(f'