scripts: Added codemapd3.py

Inspired heavily by d3 and brendangregg's flamegraphs, codemapd3.py is
intended to be a powerful high-level code exploring tool.

It's a visual tool, so probably best explained visually:

  $ CFLAGS='-DLFS_NO_LOG -DLFS_NO_ASSERT' make -j
  $ ./scripts/codemapd3.py \
          lfs.o lfs_util.o \
          lfs.ci lfs_util.ci \
          -otest.svg -W1500 -H700 --dark
  updated test.svg, code 35528 stack 2440 ctx 636

And open test.svg in a browser of your choice.

(TODO add a make rule for this)

---

Features include:

- Rendering of code cost in a treemap organized by subsystem (based on
  underscore-separated namespaces), making it relatively easy to see
  where the bulk of our code cost comes from.

- Rendering of the deepest stack/ctx cost as a set of tiles, making it
  relatively easy to see where the bulk of our stack cost comes from.

- Interactive (on mouseover) rendering of callgraph info, showing
  dependencies and relevant stack/ctx costs per-function.

  This currently includes 4 modes:

  1. mode-callgraph - This shows the full callgraph, including all
     children's children, which is effectively all dependencies of that
     function, i.e. the total code cost necessary for that _specific_
     function to work.

  2. mode-deepest - This shows the deepest/hot path of calls from that
     function, which is every child that contributes to the function's
     stack cost.

  3. mode-callees - This shows all functions the current function
     immediately calls.

  4. mode-callers - This shows all functions that call the current
     function.

  And yes, cycles are handled correctly: We show the deepest
  non-cyclical path, but display the measured stack usage as infinite.

For more details see ./scripts/codemapd3.py --help.

---

One particularly neat feature I'm happy about is -t/--tiny, which scales
the resulting image such that 1 pixel ~= 1 byte. This should be useful
for comparing littlefs to other filesystems in a way that is visually
interesting.

- d3 - https://d3js.org
- brendangregg's flamegraphs - https://github.com/brendangregg/FlameGraph
This commit is contained in:
Christopher Haster
2025-03-05 23:44:34 -06:00
parent 021e03cdc9
commit e780fd40f7
3 changed files with 2949 additions and 13 deletions
+2929
View File
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -1141,22 +1141,24 @@ if __name__ == "__main__":
parser.add_argument(
'--to-scale',
nargs='?',
type=float,
type=lambda x: (
(lambda a, b: a / b)(*(float(v) for v in x.split(':', 1)))
if ':' in x else float(x)),
const=1,
help="Scale the resulting treemap such that 1 pixel ~= 1/scale "
"units. Defaults to scale=1. ")
parser.add_argument(
'-R', '--aspect-ratio',
type=lambda x: tuple(float(v) for v in x.split(':', 1)),
default=(1, 1),
type=lambda x: (
tuple(float(v) for v in x.split(':', 1))
if ':' in x else (float(x), 1)),
help="Aspect ratio to use with --to-scale. Defaults to 1:1.")
parser.add_argument(
'--title',
help="Add a title.")
help="Add a title. Accepts %% modifiers.")
parser.add_argument(
'--padding',
type=float,
default=0,
help="Padding to add to each level of the treemap. Defaults to 0.")
parser.add_argument(
'-l', '--label',
+13 -8
View File
@@ -577,7 +577,8 @@ def main(csv_paths, output, *,
**args):
# tiny mode?
if tiny:
to_scale = True
if to_scale is None:
to_scale = 1
no_header = True
no_label = True
@@ -666,7 +667,9 @@ def main(csv_paths, output, *,
t.label = punescape(labels_[i, t.key], t.attrs)
# scale width/height if requested now that we have our data
if to_scale and (width is None or height is None) and tile.value != 0:
if (to_scale is not None
and (width is None or height is None)
and tile.value != 0):
# scale width only
if height is not None:
width_ = mt.ceil((tile.value * to_scale) / height_)
@@ -907,7 +910,7 @@ if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(
description="Render CSV files as a treemap to a d3-esque svg.",
description="Render CSV files as a d3-esque treemap.",
allow_abbrev=False)
parser.add_argument(
'csv_paths',
@@ -1022,14 +1025,17 @@ if __name__ == "__main__":
parser.add_argument(
'--to-scale',
nargs='?',
type=float,
type=lambda x: (
(lambda a, b: a / b)(*(float(v) for v in x.split(':', 1)))
if ':' in x else float(x)),
const=1,
help="Scale the resulting treemap such that 1 pixel ~= 1/scale "
"units. Defaults to scale=1. ")
parser.add_argument(
'-R', '--aspect-ratio',
type=lambda x: tuple(float(v) for v in x.split(':', 1)),
default=(1, 1),
type=lambda x: (
tuple(float(v) for v in x.split(':', 1))
if ':' in x else (float(x), 1)),
help="Aspect ratio to use with --to-scale. Defaults to 1:1.")
parser.add_argument(
'-t', '--tiny',
@@ -1042,11 +1048,10 @@ if __name__ == "__main__":
help="Show nested tiles.")
parser.add_argument(
'--title',
help="Add a title.")
help="Add a title. Accepts %% modifiers.")
parser.add_argument(
'--padding',
type=float,
default=1,
help="Padding to add to each level of the treemap. Defaults to 1.")
parser.add_argument(
'--no-label',