scripts: dbgbmap.py tweaks

- Create a grid with dashes even in -%/--usage mode.

  This was surprisingly annoying since it breaks the existing
  1 block = 1 char assumption.

- Derive percentages from in-use blocks, not all blocks. This matches
  behavior of tracebd.py's percentages (% read/prog/erase).

  Though not tracebd.py's percent wear...

- Added mdir/btree/data counts/percentages to dbgbmapd3.py, for use in
  custom --title strings and the newly added --title-usage.

  Because why not. Unlike dbgbmap.py, performance is not a concern at
  all, and the consistency between these two scripts helps
  maintainability.

  Case in point: also fixed a typo from copying the block_count
  inference between scripts.
This commit is contained in:
Christopher Haster
2025-04-10 12:37:43 -05:00
parent d5c0e142f0
commit 465fdd1fca
3 changed files with 147 additions and 72 deletions
+54 -26
View File
@@ -4299,6 +4299,7 @@ def main_(f, disk, mroots=None, *,
tiny=False, tiny=False,
title=None, title=None,
title_littlefs=False, title_littlefs=False,
title_usage=False,
padding=0, padding=0,
**args): **args):
# give f an writeln function # give f an writeln function
@@ -4415,6 +4416,7 @@ def main_(f, disk, mroots=None, *,
mdir_count = 0 mdir_count = 0
btree_count = 0 btree_count = 0
data_count = 0 data_count = 0
total_count = 0
for child in lfs.traverse( for child in lfs.traverse(
mtree_only=mtree_only): mtree_only=mtree_only):
# track each block in our window # track each block in our window
@@ -4430,6 +4432,7 @@ def main_(f, disk, mroots=None, *,
else: else:
usage = range(0) usage = range(0)
mdir_count += 1 mdir_count += 1
total_count += 1
# btree node? # btree node?
elif isinstance(child, Rbyd): elif isinstance(child, Rbyd):
@@ -4439,12 +4442,14 @@ def main_(f, disk, mroots=None, *,
else: else:
usage = range(0) usage = range(0)
btree_count += 1 btree_count += 1
total_count += 1
# bptr? # bptr?
elif isinstance(child, Bptr): elif isinstance(child, Bptr):
type = 'data' type = 'data'
usage = range(child.off, child.off+child.size) usage = range(child.off, child.off+child.size)
data_count += 1 data_count += 1
total_count += 1
else: else:
assert False, "%r?" % b assert False, "%r?" % b
@@ -4575,21 +4580,25 @@ def main_(f, disk, mroots=None, *,
# assign chars based on block type # assign chars based on block type
for b in bmap.values(): for b in bmap.values():
char__ = chars_[b.block, (b.type, '0x%x' % b.block)] b.chars = {}
for type in [b.type] + (['unused'] if args.get('usage') else []):
char__ = chars_[b.block, (type, '0x%x' % b.block)]
if char__ is not None: if char__ is not None:
# don't punescape unless we have to # don't punescape unless we have to
if '%' in char__: if '%' in char__:
char__ = punescape(char__, b.attrs) char__ = punescape(char__, b.attrs)
b.char = char__[0] # limit to 1 char b.chars[type] = char__[0] # limit to 1 char
# assign colors based on block type # assign colors based on block type
for b in bmap.values(): for b in bmap.values():
color__ = colors_[b.block, (b.type, '0x%x' % b.block)] b.colors = {}
for type in [b.type] + (['unused'] if args.get('usage') else []):
color__ = colors_[b.block, (type, '0x%x' % b.block)]
if color__ is not None: if color__ is not None:
# don't punescape unless we have to # don't punescape unless we have to
if '%' in color__: if '%' in color__:
color__ = punescape(color__, b.attrs) color__ = punescape(color__, b.attrs)
b.color = color__ b.colors[type] = color__
# render to canvas in a specific z-order that prioritizes # render to canvas in a specific z-order that prioritizes
# interesting blocks # interesting blocks
@@ -4599,7 +4608,16 @@ def main_(f, disk, mroots=None, *,
continue continue
for b in bmap.values(): for b in bmap.values():
if b.type != type: # a bit of a hack, but render all blocks as unused
# in the first pass in usage mode
if args.get('usage') and type == 'unused':
type__ = 'unused'
usage__ = range(block_size_)
else:
type__ = b.type
usage__ = b.usage
if type__ != type:
continue continue
# contiguous? # contiguous?
@@ -4607,14 +4625,14 @@ def main_(f, disk, mroots=None, *,
# where are we in the curve? # where are we in the curve?
if args.get('usage'): if args.get('usage'):
# skip blocks with no usage # skip blocks with no usage
if not b.usage: if not usage__:
continue continue
block__ = b.block - global_block block__ = b.block - global_block
usage__ = range( usage__ = range(
mt.floor(((block__*block_size_ + b.usage.start) mt.floor(((block__*block_size_ + usage__.start)
/ (block_size_ * len(bmap))) / (block_size_ * len(bmap)))
* len(global_curve)), * len(global_curve)),
mt.ceil(((block__*block_size_ + b.usage.stop) mt.ceil(((block__*block_size_ + usage__.stop)
/ (block_size_ * len(bmap))) / (block_size_ * len(bmap)))
* len(global_curve))) * len(global_curve)))
else: else:
@@ -4633,8 +4651,8 @@ def main_(f, disk, mroots=None, *,
y__ = canvas.height - (y__+1) y__ = canvas.height - (y__+1)
canvas.point(x__, y__, canvas.point(x__, y__,
char=True if braille or dots else b.char, char=True if braille or dots else b.chars[type],
color=b.color) color=b.colors[type])
# blocky? # blocky?
else: else:
@@ -4649,27 +4667,28 @@ def main_(f, disk, mroots=None, *,
# render byte-level usage? # render byte-level usage?
if args.get('usage'): if args.get('usage'):
# skip blocks with no usage # skip blocks with no usage
if not b.usage: if not usage__:
continue continue
# scale from bytes -> pixels # scale from bytes -> pixels
usage__ = range( usage__ = range(
mt.floor((b.usage.start/block_size_) mt.floor((usage__.start/block_size_)
* (width__*height__)), * (width__*height__)),
mt.ceil((b.usage.stop/block_size_) mt.ceil((usage__.stop/block_size_)
* (width__*height__))) * (width__*height__)))
# map to in-block curve # map to in-block curve
for i, (dx, dy) in enumerate(curve(width__, height__)): for i, (dx, dy) in enumerate(curve(width__, height__)):
if i in usage__: if i in usage__:
# flip y # flip y
canvas.point(x__+dx, y__+(height__-(dy+1)), canvas.point(x__+dx, y__+(height__-(dy+1)),
char=True if braille or dots else b.char, char=True if braille or dots
color=b.color) else b.chars[type],
color=b.colors[type])
# render simple blocks # render simple blocks
else: else:
canvas.rect(x__, y__, width__, height__, canvas.rect(x__, y__, width__, height__,
char=True if braille or dots else b.char, char=True if braille or dots else b.chars[type],
color=b.color) color=b.colors[type])
# print some summary info # print some summary info
if not no_header: if not no_header:
@@ -4704,12 +4723,16 @@ def main_(f, disk, mroots=None, *,
'cksum': '%08x%s' % ( 'cksum': '%08x%s' % (
lfs.cksum, lfs.cksum,
'' if lfs.ckgcksum() else '?'), '' if lfs.ckgcksum() else '?'),
'mdir_count': mdir_count, 'total': total_count,
'mdir_percent': '%.1f%%' % (100*(mdir_count / len(bmap))), 'mdir': mdir_count,
'btree_count': btree_count, 'mdir_percent': '%.1f%%' % (
'btree_percent': '%.1f%%' % (100*(btree_count / len(bmap))), 100*(mdir_count / max(total_count, 1))),
'data_count': data_count, 'btree': btree_count,
'data_percent': '%.1f%%' % (100*(data_count / len(bmap))), 'btree_percent': '%.1f%%' % (
100*(btree_count / max(total_count, 1))),
'data': data_count,
'data_percent': '%.1f%%' % (
100*(data_count / max(total_count, 1))),
})) }))
elif title_littlefs: elif title_littlefs:
f.writeln('littlefs%s v%s.%s %sx%s %s w%s.%s, cksum %08x%s' % ( f.writeln('littlefs%s v%s.%s %sx%s %s w%s.%s, cksum %08x%s' % (
@@ -4726,9 +4749,9 @@ def main_(f, disk, mroots=None, *,
f.writeln('bd %sx%s, %6s mdir, %6s btree, %6s data' % ( f.writeln('bd %sx%s, %6s mdir, %6s btree, %6s data' % (
lfs.block_size if lfs.block_size is not None else '?', lfs.block_size if lfs.block_size is not None else '?',
lfs.block_count if lfs.block_count is not None else '?', lfs.block_count if lfs.block_count is not None else '?',
'%.1f%%' % (100*(mdir_count / len(bmap))), '%.1f%%' % (100*(mdir_count / max(total_count, 1))),
'%.1f%%' % (100*(btree_count / len(bmap))), '%.1f%%' % (100*(btree_count / max(total_count, 1))),
'%.1f%%' % (100*(data_count / len(bmap))))) '%.1f%%' % (100*(data_count / max(total_count, 1)))))
# draw canvas # draw canvas
for row in range(canvas.height//canvas.yscale): for row in range(canvas.height//canvas.yscale):
@@ -4980,6 +5003,11 @@ if __name__ == "__main__":
'--title-littlefs', '--title-littlefs',
action='store_true', action='store_true',
help="Use the littlefs mount string as the title.") help="Use the littlefs mount string as the title.")
parser.add_argument(
'--title-usage',
action='store_true',
help="Use the mdir/btree/data usage as the title. This is the "
"default.")
# TODO drop padding in ascii scripts, no one is ever going to use this # TODO drop padding in ascii scripts, no one is ever going to use this
parser.add_argument( parser.add_argument(
'--padding', '--padding',
+41 -2
View File
@@ -16,6 +16,7 @@ import functools as ft
import itertools as it import itertools as it
import json import json
import math as mt import math as mt
import os
import re import re
import shlex import shlex
import struct import struct
@@ -4628,6 +4629,8 @@ def main(disk, output, mroots=None, *,
aspect_ratio=(1,1), aspect_ratio=(1,1),
tiny=False, tiny=False,
title=None, title=None,
title_littlefs=False,
title_usage=False,
padding=None, padding=None,
no_label=False, no_label=False,
dark=False, dark=False,
@@ -4728,7 +4731,7 @@ def main(disk, output, mroots=None, *,
block_count_ = lfs.config.geometry.block_count block_count_ = lfs.config.geometry.block_count
else: else:
f.seek(0, os.SEEK_END) f.seek(0, os.SEEK_END)
block_count_ = mt.ceil(f_.tell() / block_size) block_count_ = mt.ceil(f.tell() / block_size)
# flatten blocks, default to all blocks # flatten blocks, default to all blocks
blocks_ = list( blocks_ = list(
@@ -4740,6 +4743,10 @@ def main(disk, output, mroots=None, *,
# traverse the filesystem and create a block map # traverse the filesystem and create a block map
bmap = {b: BmapBlock(b, 'unused') for b in blocks_} bmap = {b: BmapBlock(b, 'unused') for b in blocks_}
mdir_count = 0
btree_count = 0
data_count = 0
total_count = 0
for child, path in lfs.traverse( for child, path in lfs.traverse(
mtree_only=mtree_only, mtree_only=mtree_only,
path=True): path=True):
@@ -4755,6 +4762,8 @@ def main(disk, output, mroots=None, *,
usage = range(child.eoff) usage = range(child.eoff)
else: else:
usage = range(0) usage = range(0)
mdir_count += 1
total_count += 1
# btree node? # btree node?
elif isinstance(child, Rbyd): elif isinstance(child, Rbyd):
@@ -4763,11 +4772,15 @@ def main(disk, output, mroots=None, *,
usage = range(child.eoff) usage = range(child.eoff)
else: else:
usage = range(0) usage = range(0)
btree_count += 1
total_count += 1
# bptr? # bptr?
elif isinstance(child, Bptr): elif isinstance(child, Bptr):
type = 'data' type = 'data'
usage = range(child.off, child.off+child.size) usage = range(child.off, child.off+child.size)
data_count += 1
total_count += 1
else: else:
assert False, "%r?" % b assert False, "%r?" % b
@@ -4993,8 +5006,18 @@ def main(disk, output, mroots=None, *,
'cksum': '%08x%s' % ( 'cksum': '%08x%s' % (
lfs.cksum, lfs.cksum,
'' if lfs.ckgcksum() else '?'), '' if lfs.ckgcksum() else '?'),
'total': total_count,
'mdir': mdir_count,
'mdir_percent': '%.1f%%' % (
100*(mdir_count / max(total_count, 1))),
'btree': btree_count,
'btree_percent': '%.1f%%' % (
100*(btree_count / max(total_count, 1))),
'data': data_count,
'data_percent': '%.1f%%' % (
100*(data_count / max(total_count, 1))),
})) }))
else: elif not title_usage:
f.write('littlefs%s v%s.%s %sx%s %s w%s.%s, cksum %08x%s' % ( f.write('littlefs%s v%s.%s %sx%s %s w%s.%s, cksum %08x%s' % (
'' if lfs.ckmagic() else '?', '' if lfs.ckmagic() else '?',
lfs.version.major if lfs.version is not None else '?', lfs.version.major if lfs.version is not None else '?',
@@ -5005,6 +5028,13 @@ def main(disk, output, mroots=None, *,
lfs.mbweightrepr(), lfs.mrweightrepr(), lfs.mbweightrepr(), lfs.mrweightrepr(),
lfs.cksum, lfs.cksum,
'' if lfs.ckgcksum() else '?')) '' if lfs.ckgcksum() else '?'))
else:
f.writeln('bd %sx%s, %s mdir, %s btree, %s data' % (
lfs.block_size if lfs.block_size is not None else '?',
lfs.block_count if lfs.block_count is not None else '?',
'%.1f%%' % (100*(mdir_count / max(total_count, 1))),
'%.1f%%' % (100*(btree_count / max(total_count, 1))),
'%.1f%%' % (100*(data_count / max(total_count, 1)))))
f.write('</tspan>') f.write('</tspan>')
if not no_mode and not no_javascript: if not no_mode and not no_javascript:
f.write('<tspan id="mode" x="%(x)d" y="1.1em" ' f.write('<tspan id="mode" x="%(x)d" y="1.1em" '
@@ -5894,6 +5924,15 @@ if __name__ == "__main__":
parser.add_argument( parser.add_argument(
'--title', '--title',
help="Add a title. Accepts %% modifiers.") help="Add a title. Accepts %% modifiers.")
parser.add_argument(
'--title-littlefs',
action='store_true',
help="Use the littlefs mount string as the title. This is the "
"default.")
parser.add_argument(
'--title-usage',
action='store_true',
help="Use the mdir/btree/data usage as the title.")
parser.add_argument( parser.add_argument(
'--padding', '--padding',
type=float, type=float,
+31 -23
View File
@@ -1665,7 +1665,7 @@ def main(path='-', *,
if block not in bmap: if block not in bmap:
return False return False
else: else:
bmap[block].erase(0, block_count_, bmap[block].erase(0, block_size_,
wear=+1 if wear else 0) wear=+1 if wear else 0)
erased += block_count_ erased += block_count_
return True return True
@@ -1691,6 +1691,25 @@ def main(path='-', *,
if bmap is None: if bmap is None:
return return
# compute total ops
total = readed + proged + erased
# if we're showing wear, find min/max/avg/etc
if wear:
wear_min = min(b.wear for b in bmap.values())
wear_max = max(b.wear for b in bmap.values())
wear_avg = (sum(b.wear for b in bmap.values())
/ max(len(bmap), 1))
wear_stddev = mt.sqrt(
sum((b.wear - wear_avg)**2 for b in bmap.values())
/ max(len(bmap), 1))
# if block_cycles isn't provide, scale based on max wear
if block_cycles is not None:
block_cycles_ = block_cycles
else:
block_cycles_ = wear_max
# give ring a writeln function # give ring a writeln function
def writeln(s=''): def writeln(s=''):
ring.write(s) ring.write(s)
@@ -1799,10 +1818,14 @@ def main(path='-', *,
b.height = max(b.height, 1) b.height = max(b.height, 1)
# TODO chars should probably take priority over braille/dots # TODO chars should probably take priority over braille/dots
# TODO limit these based on requested ops?
# assign chars based on op + block # assign chars based on op + block
for b in bmap.values(): for b in bmap.values():
b.chars = {} b.chars = {}
for op in ['read', 'prog', 'erase', 'noop']: for op in ((['read'] if reads else [])
+ (['prog'] if progs else [])
+ (['erase'] if erases else [])
+ ['noop']):
char__ = chars_.get((b.block, (op, '0x%x' % b.block))) char__ = chars_.get((b.block, (op, '0x%x' % b.block)))
if char__ is not None: if char__ is not None:
# don't punescape unless we have to # don't punescape unless we have to
@@ -1813,7 +1836,10 @@ def main(path='-', *,
# assign colors based on op + block # assign colors based on op + block
for b in bmap.values(): for b in bmap.values():
b.colors = {} b.colors = {}
for op in ['read', 'prog', 'erase', 'noop']: for op in ((['read'] if reads else [])
+ (['prog'] if progs else [])
+ (['erase'] if erases else [])
+ ['noop']):
color__ = colors_.get((b.block, (op, '0x%x' % b.block))) color__ = colors_.get((b.block, (op, '0x%x' % b.block)))
if color__ is not None: if color__ is not None:
# don't punescape unless we have to # don't punescape unless we have to
@@ -1822,6 +1848,7 @@ def main(path='-', *,
b.colors[op] = color__ b.colors[op] = color__
# assign wear chars based on block # assign wear chars based on block
if wear:
for b in bmap.values(): for b in bmap.values():
b.wear_chars = [] b.wear_chars = []
for char__ in wear_chars_.getall((b.block, '0x%x' % b.block)): for char__ in wear_chars_.getall((b.block, '0x%x' % b.block)):
@@ -1831,6 +1858,7 @@ def main(path='-', *,
b.wear_chars.append(char__[0]) # limit to 1 char b.wear_chars.append(char__[0]) # limit to 1 char
# assign wear colors based on block # assign wear colors based on block
if wear:
for b in bmap.values(): for b in bmap.values():
b.wear_colors = [] b.wear_colors = []
for color__ in wear_colors_.getall((b.block, '0x%x' % b.block)): for color__ in wear_colors_.getall((b.block, '0x%x' % b.block)):
@@ -1839,25 +1867,6 @@ def main(path='-', *,
color__ = punescape(color__, b.attrs) color__ = punescape(color__, b.attrs)
b.wear_colors.append(color__) b.wear_colors.append(color__)
# compute total ops
total = readed + proged + erased
# if we're showing wear, find min/max/avg/etc
if wear:
wear_min = min(b.wear for b in bmap.values())
wear_max = max(b.wear for b in bmap.values())
wear_avg = (sum(b.wear for b in bmap.values())
/ max(len(bmap), 1))
wear_stddev = mt.sqrt(
sum((b.wear - wear_avg)**2 for b in bmap.values())
/ max(len(bmap), 1))
# if block_cycles isn't provide, scale based on max wear
if block_cycles is not None:
block_cycles_ = block_cycles
else:
block_cycles_ = wear_max
# render to canvas in a specific z-order that prioritizes # render to canvas in a specific z-order that prioritizes
# interesting ops # interesting ops
for op in reversed(Z_ORDER): for op in reversed(Z_ORDER):
@@ -1957,7 +1966,6 @@ def main(path='-', *,
# print some summary info # print some summary info
if not no_header: if not no_header:
# TODO # TODO
# # compute stddev of wear using our bmap, this is a bit different # # compute stddev of wear using our bmap, this is a bit different
# # from reads/progs/erases which ignores any bmap window, but it's # # from reads/progs/erases which ignores any bmap window, but it's