tracebd.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962
  1. #!/usr/bin/env python3
  2. #
  3. # Display operations on block devices based on trace output
  4. #
  5. # Example:
  6. # ./scripts/tracebd.py trace
  7. #
  8. # Copyright (c) 2022, The littlefs authors.
  9. # SPDX-License-Identifier: BSD-3-Clause
  10. #
  11. import collections as co
  12. import functools as ft
  13. import io
  14. import itertools as it
  15. import math as m
  16. import os
  17. import re
  18. import shutil
  19. import time
  20. CHARS = 'rpe.'
  21. COLORS = ['42', '45', '44', '']
  22. WEAR_CHARS = '0123456789'
  23. WEAR_CHARS_SUBSCRIPTS = '.₁₂₃₄₅₆789'
  24. WEAR_COLORS = ['', '', '', '', '', '', '', '35', '35', '1;31']
  25. CHARS_DOTS = " .':"
  26. COLORS_DOTS = ['32', '35', '34', '']
  27. CHARS_BRAILLE = (
  28. '⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤' '⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴'
  29. '⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦' '⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶'
  30. '⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬' '⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼'
  31. '⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮' '⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾'
  32. '⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥' '⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵'
  33. '⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧' '⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷'
  34. '⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭' '⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽'
  35. '⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯' '⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿')
  36. def openio(path, mode='r'):
  37. if path == '-':
  38. if mode == 'r':
  39. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  40. else:
  41. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  42. else:
  43. return open(path, mode)
  44. class LinesIO:
  45. def __init__(self, maxlen=None):
  46. self.maxlen = maxlen
  47. self.lines = co.deque(maxlen=maxlen)
  48. self.tail = io.StringIO()
  49. # trigger automatic sizing
  50. if maxlen == 0:
  51. self.resize(0)
  52. def write(self, s):
  53. # note using split here ensures the trailing string has no newline
  54. lines = s.split('\n')
  55. if len(lines) > 1 and self.tail.getvalue():
  56. self.tail.write(lines[0])
  57. lines[0] = self.tail.getvalue()
  58. self.tail = io.StringIO()
  59. self.lines.extend(lines[:-1])
  60. if lines[-1]:
  61. self.tail.write(lines[-1])
  62. def resize(self, maxlen):
  63. self.maxlen = maxlen
  64. if maxlen == 0:
  65. maxlen = shutil.get_terminal_size((80, 5))[1]
  66. if maxlen != self.lines.maxlen:
  67. self.lines = co.deque(self.lines, maxlen=maxlen)
  68. last_lines = 1
  69. def draw(self):
  70. # did terminal size change?
  71. if self.maxlen == 0:
  72. self.resize(0)
  73. # first thing first, give ourself a canvas
  74. while LinesIO.last_lines < len(self.lines):
  75. sys.stdout.write('\n')
  76. LinesIO.last_lines += 1
  77. for j, line in enumerate(self.lines):
  78. # move cursor, clear line, disable/reenable line wrapping
  79. sys.stdout.write('\r')
  80. if len(self.lines)-1-j > 0:
  81. sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-j))
  82. sys.stdout.write('\x1b[K')
  83. sys.stdout.write('\x1b[?7l')
  84. sys.stdout.write(line)
  85. sys.stdout.write('\x1b[?7h')
  86. if len(self.lines)-1-j > 0:
  87. sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-j))
  88. sys.stdout.flush()
  89. # space filling Hilbert-curve
  90. #
  91. # note we memoize the last curve since this is a bit expensive
  92. #
  93. @ft.lru_cache(1)
  94. def hilbert_curve(width, height):
  95. # based on generalized Hilbert curves:
  96. # https://github.com/jakubcerveny/gilbert
  97. #
  98. def hilbert_(x, y, a_x, a_y, b_x, b_y):
  99. w = abs(a_x+a_y)
  100. h = abs(b_x+b_y)
  101. a_dx = -1 if a_x < 0 else +1 if a_x > 0 else 0
  102. a_dy = -1 if a_y < 0 else +1 if a_y > 0 else 0
  103. b_dx = -1 if b_x < 0 else +1 if b_x > 0 else 0
  104. b_dy = -1 if b_y < 0 else +1 if b_y > 0 else 0
  105. # trivial row
  106. if h == 1:
  107. for _ in range(w):
  108. yield (x,y)
  109. x, y = x+a_dx, y+a_dy
  110. return
  111. # trivial column
  112. if w == 1:
  113. for _ in range(h):
  114. yield (x,y)
  115. x, y = x+b_dx, y+b_dy
  116. return
  117. a_x_, a_y_ = a_x//2, a_y//2
  118. b_x_, b_y_ = b_x//2, b_y//2
  119. w_ = abs(a_x_+a_y_)
  120. h_ = abs(b_x_+b_y_)
  121. if 2*w > 3*h:
  122. # prefer even steps
  123. if w_ % 2 != 0 and w > 2:
  124. a_x_, a_y_ = a_x_+a_dx, a_y_+a_dy
  125. # split in two
  126. yield from hilbert_(x, y, a_x_, a_y_, b_x, b_y)
  127. yield from hilbert_(x+a_x_, y+a_y_, a_x-a_x_, a_y-a_y_, b_x, b_y)
  128. else:
  129. # prefer even steps
  130. if h_ % 2 != 0 and h > 2:
  131. b_x_, b_y_ = b_x_+b_dx, b_y_+b_dy
  132. # split in three
  133. yield from hilbert_(x, y, b_x_, b_y_, a_x_, a_y_)
  134. yield from hilbert_(x+b_x_, y+b_y_, a_x, a_y, b_x-b_x_, b_y-b_y_)
  135. yield from hilbert_(
  136. x+(a_x-a_dx)+(b_x_-b_dx), y+(a_y-a_dy)+(b_y_-b_dy),
  137. -b_x_, -b_y_, -(a_x-a_x_), -(a_y-a_y_))
  138. if width >= height:
  139. curve = hilbert_(0, 0, +width, 0, 0, +height)
  140. else:
  141. curve = hilbert_(0, 0, 0, +height, +width, 0)
  142. return list(curve)
  143. # space filling Z-curve/Lebesgue-curve
  144. #
  145. # note we memoize the last curve since this is a bit expensive
  146. #
  147. @ft.lru_cache(1)
  148. def lebesgue_curve(width, height):
  149. # we create a truncated Z-curve by simply filtering out the points
  150. # that are outside our region
  151. curve = []
  152. for i in range(2**(2*m.ceil(m.log2(max(width, height))))):
  153. # we just operate on binary strings here because it's easier
  154. b = '{:0{}b}'.format(i, 2*m.ceil(m.log2(i+1)/2))
  155. x = int(b[1::2], 2) if b[1::2] else 0
  156. y = int(b[0::2], 2) if b[0::2] else 0
  157. if x < width and y < height:
  158. curve.append((x, y))
  159. return curve
  160. class Block(int):
  161. __slots__ = ()
  162. def __new__(cls, state=0, *,
  163. wear=0,
  164. readed=False,
  165. proged=False,
  166. erased=False):
  167. return super().__new__(cls,
  168. state
  169. | (wear << 3)
  170. | (1 if readed else 0)
  171. | (2 if proged else 0)
  172. | (4 if erased else 0))
  173. @property
  174. def wear(self):
  175. return self >> 3
  176. @property
  177. def readed(self):
  178. return (self & 1) != 0
  179. @property
  180. def proged(self):
  181. return (self & 2) != 0
  182. @property
  183. def erased(self):
  184. return (self & 4) != 0
  185. def read(self):
  186. return Block(int(self) | 1)
  187. def prog(self):
  188. return Block(int(self) | 2)
  189. def erase(self):
  190. return Block((int(self) | 4) + 8)
  191. def clear(self):
  192. return Block(int(self) & ~7)
  193. def __or__(self, other):
  194. return Block(
  195. (int(self) | int(other)) & 7,
  196. wear=max(self.wear, other.wear))
  197. def worn(self, max_wear, *,
  198. block_cycles=None,
  199. wear_chars=None,
  200. **_):
  201. if wear_chars is None:
  202. wear_chars = WEAR_CHARS
  203. if block_cycles:
  204. return self.wear / block_cycles
  205. else:
  206. return self.wear / max(max_wear, len(wear_chars))
  207. def draw(self, max_wear, char=None, *,
  208. read=True,
  209. prog=True,
  210. erase=True,
  211. wear=False,
  212. block_cycles=None,
  213. color=True,
  214. subscripts=False,
  215. dots=False,
  216. braille=False,
  217. chars=None,
  218. wear_chars=None,
  219. colors=None,
  220. wear_colors=None,
  221. **_):
  222. # fallback to default chars/colors
  223. if chars is None:
  224. chars = CHARS
  225. if len(chars) < len(CHARS):
  226. chars = chars + CHARS[len(chars):]
  227. if colors is None:
  228. if braille or dots:
  229. colors = COLORS_DOTS
  230. else:
  231. colors = COLORS
  232. if len(colors) < len(COLORS):
  233. colors = colors + COLORS[len(colors):]
  234. if wear_chars is None:
  235. if subscripts:
  236. wear_chars = WEAR_CHARS_SUBSCRIPTS
  237. else:
  238. wear_chars = WEAR_CHARS
  239. if wear_colors is None:
  240. wear_colors = WEAR_COLORS
  241. # compute char/color
  242. c = chars[3]
  243. f = [colors[3]]
  244. if wear:
  245. w = min(
  246. self.worn(
  247. max_wear,
  248. block_cycles=block_cycles,
  249. wear_chars=wear_chars),
  250. 1)
  251. c = wear_chars[int(w * (len(wear_chars)-1))]
  252. f.append(wear_colors[int(w * (len(wear_colors)-1))])
  253. if erase and self.erased:
  254. c = chars[2]
  255. f.append(colors[2])
  256. elif prog and self.proged:
  257. c = chars[1]
  258. f.append(colors[1])
  259. elif read and self.readed:
  260. c = chars[0]
  261. f.append(colors[0])
  262. # override char?
  263. if char:
  264. c = char
  265. # apply colors
  266. if f and color:
  267. c = '%s%s\x1b[m' % (
  268. ''.join('\x1b[%sm' % f_ for f_ in f),
  269. c)
  270. return c
  271. class Bd:
  272. def __init__(self, *,
  273. size=1,
  274. count=1,
  275. width=None,
  276. height=1,
  277. blocks=None):
  278. if width is None:
  279. width = count
  280. if blocks is None:
  281. self.blocks = [Block() for _ in range(width*height)]
  282. else:
  283. self.blocks = blocks
  284. self.size = size
  285. self.count = count
  286. self.width = width
  287. self.height = height
  288. def _op(self, f, block=None, off=None, size=None):
  289. if block is None:
  290. range_ = range(len(self.blocks))
  291. else:
  292. if off is None:
  293. off, size = 0, self.size
  294. elif size is None:
  295. off, size = 0, off
  296. # update our geometry? this will do nothing if we haven't changed
  297. self.resize(
  298. size=max(self.size, off+size),
  299. count=max(self.count, block+1))
  300. # map to our block space
  301. start = (block*self.size + off) / (self.size*self.count)
  302. stop = (block*self.size + off+size) / (self.size*self.count)
  303. range_ = range(
  304. m.floor(start*len(self.blocks)),
  305. m.ceil(stop*len(self.blocks)))
  306. # apply the op
  307. for i in range_:
  308. self.blocks[i] = f(self.blocks[i])
  309. def read(self, block=None, off=None, size=None):
  310. self._op(Block.read, block, off, size)
  311. def prog(self, block=None, off=None, size=None):
  312. self._op(Block.prog, block, off, size)
  313. def erase(self, block=None, off=None, size=None):
  314. self._op(Block.erase, block, off, size)
  315. def clear(self, block=None, off=None, size=None):
  316. self._op(Block.clear, block, off, size)
  317. def copy(self):
  318. return Bd(
  319. blocks=self.blocks.copy(),
  320. size=self.size,
  321. count=self.count,
  322. width=self.width,
  323. height=self.height)
  324. def resize(self, *,
  325. size=None,
  326. count=None,
  327. width=None,
  328. height=None):
  329. size = size if size is not None else self.size
  330. count = count if count is not None else self.count
  331. width = width if width is not None else self.width
  332. height = height if height is not None else self.height
  333. if (size == self.size
  334. and count == self.count
  335. and width == self.width
  336. and height == self.height):
  337. return
  338. # transform our blocks
  339. blocks = []
  340. for x in range(width*height):
  341. # map from new bd space
  342. start = m.floor(x * (size*count)/(width*height))
  343. stop = m.ceil((x+1) * (size*count)/(width*height))
  344. start_block = start // size
  345. start_off = start % size
  346. stop_block = stop // size
  347. stop_off = stop % size
  348. # map to old bd space
  349. start = start_block*self.size + start_off
  350. stop = stop_block*self.size + stop_off
  351. start = m.floor(start * len(self.blocks)/(self.size*self.count))
  352. stop = m.ceil(stop * len(self.blocks)/(self.size*self.count))
  353. # aggregate state
  354. blocks.append(ft.reduce(
  355. Block.__or__,
  356. self.blocks[start:stop],
  357. Block()))
  358. self.size = size
  359. self.count = count
  360. self.width = width
  361. self.height = height
  362. self.blocks = blocks
  363. def draw(self, row, *,
  364. read=False,
  365. prog=False,
  366. erase=False,
  367. wear=False,
  368. hilbert=False,
  369. lebesgue=False,
  370. dots=False,
  371. braille=False,
  372. **args):
  373. # find max wear?
  374. max_wear = None
  375. if wear:
  376. max_wear = max(b.wear for b in self.blocks)
  377. # fold via a curve?
  378. if hilbert:
  379. grid = [None]*(self.width*self.height)
  380. for (x,y), b in zip(
  381. hilbert_curve(self.width, self.height),
  382. self.blocks):
  383. grid[x + y*self.width] = b
  384. elif lebesgue:
  385. grid = [None]*(self.width*self.height)
  386. for (x,y), b in zip(
  387. lebesgue_curve(self.width, self.height),
  388. self.blocks):
  389. grid[x + y*self.width] = b
  390. else:
  391. grid = self.blocks
  392. # need to wait for more trace output before rendering
  393. #
  394. # this is sort of a hack that knows the output is going to a terminal
  395. if (braille and self.height < 4) or (dots and self.height < 2):
  396. needed_height = 4 if braille else 2
  397. self.history = getattr(self, 'history', [])
  398. self.history.append(grid)
  399. if len(self.history)*self.height < needed_height:
  400. # skip for now
  401. return None
  402. grid = list(it.chain.from_iterable(
  403. # did we resize?
  404. it.islice(it.chain(h, it.repeat(Block())),
  405. self.width*self.height)
  406. for h in self.history))
  407. self.history = []
  408. line = []
  409. if braille:
  410. # encode into a byte
  411. for x in range(0, self.width, 2):
  412. byte_b = 0
  413. best_b = Block()
  414. for i in range(2*4):
  415. b = grid[x+(2-1-(i%2)) + ((row*4)+(4-1-(i//2)))*self.width]
  416. best_b |= b
  417. if ((read and b.readed)
  418. or (prog and b.proged)
  419. or (erase and b.erased)
  420. or (not read and not prog and not erase
  421. and wear and b.worn(max_wear, **args) >= 0.7)):
  422. byte_b |= 1 << i
  423. line.append(best_b.draw(
  424. max_wear,
  425. CHARS_BRAILLE[byte_b],
  426. braille=True,
  427. read=read,
  428. prog=prog,
  429. erase=erase,
  430. wear=wear,
  431. **args))
  432. elif dots:
  433. # encode into a byte
  434. for x in range(self.width):
  435. byte_b = 0
  436. best_b = Block()
  437. for i in range(2):
  438. b = grid[x + ((row*2)+(2-1-i))*self.width]
  439. best_b |= b
  440. if ((read and b.readed)
  441. or (prog and b.proged)
  442. or (erase and b.erased)
  443. or (not read and not prog and not erase
  444. and wear and b.worn(max_wear, **args) >= 0.7)):
  445. byte_b |= 1 << i
  446. line.append(best_b.draw(
  447. max_wear,
  448. CHARS_DOTS[byte_b],
  449. dots=True,
  450. read=read,
  451. prog=prog,
  452. erase=erase,
  453. wear=wear,
  454. **args))
  455. else:
  456. for x in range(self.width):
  457. line.append(grid[x + row*self.width].draw(
  458. max_wear,
  459. read=read,
  460. prog=prog,
  461. erase=erase,
  462. wear=wear,
  463. **args))
  464. return ''.join(line)
  465. def main(path='-', *,
  466. read=False,
  467. prog=False,
  468. erase=False,
  469. wear=False,
  470. block=(None,None),
  471. off=(None,None),
  472. block_size=None,
  473. block_count=None,
  474. block_cycles=None,
  475. reset=False,
  476. color='auto',
  477. dots=False,
  478. braille=False,
  479. width=None,
  480. height=None,
  481. lines=None,
  482. cat=False,
  483. hilbert=False,
  484. lebesgue=False,
  485. coalesce=None,
  486. sleep=None,
  487. keep_open=False,
  488. **args):
  489. # figure out what color should be
  490. if color == 'auto':
  491. color = sys.stdout.isatty()
  492. elif color == 'always':
  493. color = True
  494. else:
  495. color = False
  496. # exclusive wear or read/prog/erase by default
  497. if not read and not prog and not erase and not wear:
  498. read = True
  499. prog = True
  500. erase = True
  501. # assume a reasonable lines/height if not specified
  502. #
  503. # note that we let height = None if neither hilbert or lebesgue
  504. # are specified, this is a bit special as the default may be less
  505. # than one character in height.
  506. if height is None and (hilbert or lebesgue):
  507. if lines is not None:
  508. height = lines
  509. else:
  510. height = 5
  511. if lines is None:
  512. if height is not None:
  513. lines = height
  514. else:
  515. lines = 5
  516. # allow ranges for blocks/offs
  517. block_start = block[0]
  518. block_stop = block[1] if len(block) > 1 else block[0]+1
  519. off_start = off[0]
  520. off_stop = off[1] if len(off) > 1 else off[0]+1
  521. if block_start is None:
  522. block_start = 0
  523. if block_stop is None and block_count is not None:
  524. block_stop = block_count
  525. if off_start is None:
  526. off_start = 0
  527. if off_stop is None and block_size is not None:
  528. off_stop = block_size
  529. # create a block device representation
  530. bd = Bd()
  531. def resize(*, size=None, count=None):
  532. nonlocal bd
  533. # size may be overriden by cli args
  534. if block_size is not None:
  535. size = block_size
  536. elif off_stop is not None:
  537. size = off_stop-off_start
  538. if block_count is not None:
  539. count = block_count
  540. elif block_stop is not None:
  541. count = block_stop-block_start
  542. # figure out best width/height
  543. if width is None:
  544. width_ = min(80, shutil.get_terminal_size((80, 5))[0])
  545. elif width:
  546. width_ = width
  547. else:
  548. width_ = shutil.get_terminal_size((80, 5))[0]
  549. if height is None:
  550. height_ = 0
  551. elif height:
  552. height_ = height
  553. else:
  554. height_ = shutil.get_terminal_size((80, 5))[1]
  555. bd.resize(
  556. size=size,
  557. count=count,
  558. # scale if we're printing with dots or braille
  559. width=2*width_ if braille else width_,
  560. height=max(1,
  561. 4*height_ if braille
  562. else 2*height_ if dots
  563. else height_))
  564. resize()
  565. # parse a line of trace output
  566. pattern = re.compile(
  567. 'trace.*?bd_(?:'
  568. '(?P<create>create\w*)\('
  569. '(?:'
  570. 'block_size=(?P<block_size>\w+)'
  571. '|' 'block_count=(?P<block_count>\w+)'
  572. '|' '.*?' ')*' '\)'
  573. '|' '(?P<read>read)\('
  574. '\s*(?P<read_ctx>\w+)\s*' ','
  575. '\s*(?P<read_block>\w+)\s*' ','
  576. '\s*(?P<read_off>\w+)\s*' ','
  577. '\s*(?P<read_buffer>\w+)\s*' ','
  578. '\s*(?P<read_size>\w+)\s*' '\)'
  579. '|' '(?P<prog>prog)\('
  580. '\s*(?P<prog_ctx>\w+)\s*' ','
  581. '\s*(?P<prog_block>\w+)\s*' ','
  582. '\s*(?P<prog_off>\w+)\s*' ','
  583. '\s*(?P<prog_buffer>\w+)\s*' ','
  584. '\s*(?P<prog_size>\w+)\s*' '\)'
  585. '|' '(?P<erase>erase)\('
  586. '\s*(?P<erase_ctx>\w+)\s*' ','
  587. '\s*(?P<erase_block>\w+)\s*' '\)'
  588. '|' '(?P<sync>sync)\('
  589. '\s*(?P<sync_ctx>\w+)\s*' '\)' ')')
  590. def parse(line):
  591. nonlocal bd
  592. # string searching is much faster than the regex here, and this
  593. # actually has a big impact given how much trace output comes
  594. # through here
  595. if 'trace' not in line or 'bd' not in line:
  596. return False
  597. m = pattern.search(line)
  598. if not m:
  599. return False
  600. if m.group('create'):
  601. # update our block size/count
  602. size = int(m.group('block_size'), 0)
  603. count = int(m.group('block_count'), 0)
  604. resize(size=size, count=count)
  605. if reset:
  606. bd = Bd(
  607. size=bd.size,
  608. count=bd.count,
  609. width=bd.width,
  610. height=bd.height)
  611. return True
  612. elif m.group('read') and read:
  613. block = int(m.group('read_block'), 0)
  614. off = int(m.group('read_off'), 0)
  615. size = int(m.group('read_size'), 0)
  616. if block_stop is not None and block >= block_stop:
  617. return False
  618. block -= block_start
  619. if off_stop is not None:
  620. if off >= off_stop:
  621. return False
  622. size = min(size, off_stop-off)
  623. off -= off_start
  624. bd.read(block, off, size)
  625. return True
  626. elif m.group('prog') and prog:
  627. block = int(m.group('prog_block'), 0)
  628. off = int(m.group('prog_off'), 0)
  629. size = int(m.group('prog_size'), 0)
  630. if block_stop is not None and block >= block_stop:
  631. return False
  632. block -= block_start
  633. if off_stop is not None:
  634. if off >= off_stop:
  635. return False
  636. size = min(size, off_stop-off)
  637. off -= off_start
  638. bd.prog(block, off, size)
  639. return True
  640. elif m.group('erase') and (erase or wear):
  641. block = int(m.group('erase_block'), 0)
  642. if block_stop is not None and block >= block_stop:
  643. return False
  644. block -= block_start
  645. bd.erase(block)
  646. return True
  647. else:
  648. return False
  649. # print trace output
  650. def draw(f):
  651. def writeln(s=''):
  652. f.write(s)
  653. f.write('\n')
  654. f.writeln = writeln
  655. # don't forget we've scaled this for braille/dots!
  656. for row in range(
  657. m.ceil(bd.height/4) if braille
  658. else m.ceil(bd.height/2) if dots
  659. else bd.height):
  660. line = bd.draw(row,
  661. read=read,
  662. prog=prog,
  663. erase=erase,
  664. wear=wear,
  665. block_cycles=block_cycles,
  666. color=color,
  667. dots=dots,
  668. braille=braille,
  669. hilbert=hilbert,
  670. lebesgue=lebesgue,
  671. **args)
  672. if line:
  673. f.writeln(line)
  674. bd.clear()
  675. resize()
  676. # read/parse/coalesce operations
  677. if cat:
  678. ring = sys.stdout
  679. else:
  680. ring = LinesIO(lines)
  681. ptime = time.time()
  682. try:
  683. while True:
  684. with openio(path) as f:
  685. changed = 0
  686. for line in f:
  687. changed += parse(line)
  688. # need to redraw?
  689. if (changed
  690. and (not coalesce or changed >= coalesce)
  691. and (not sleep or time.time()-ptime >= sleep)):
  692. draw(ring)
  693. if not cat:
  694. ring.draw()
  695. changed = 0
  696. ptime = time.time()
  697. if not keep_open:
  698. break
  699. # don't just flood open calls
  700. time.sleep(sleep or 0.1)
  701. except KeyboardInterrupt:
  702. pass
  703. if not cat:
  704. sys.stdout.write('\n')
  705. if __name__ == "__main__":
  706. import sys
  707. import argparse
  708. parser = argparse.ArgumentParser(
  709. description="Display operations on block devices based on "
  710. "trace output.",
  711. allow_abbrev=False)
  712. parser.add_argument(
  713. 'path',
  714. nargs='?',
  715. help="Path to read from.")
  716. parser.add_argument(
  717. '-r', '--read',
  718. action='store_true',
  719. help="Render reads.")
  720. parser.add_argument(
  721. '-p', '--prog',
  722. action='store_true',
  723. help="Render progs.")
  724. parser.add_argument(
  725. '-e', '--erase',
  726. action='store_true',
  727. help="Render erases.")
  728. parser.add_argument(
  729. '-w', '--wear',
  730. action='store_true',
  731. help="Render wear.")
  732. parser.add_argument(
  733. '-b', '--block',
  734. type=lambda x: tuple(
  735. int(x, 0) if x.strip() else None
  736. for x in x.split(',')),
  737. help="Show a specific block or range of blocks.")
  738. parser.add_argument(
  739. '-i', '--off',
  740. type=lambda x: tuple(
  741. int(x, 0) if x.strip() else None
  742. for x in x.split(',')),
  743. help="Show a specific offset or range of offsets.")
  744. parser.add_argument(
  745. '-B', '--block-size',
  746. type=lambda x: int(x, 0),
  747. help="Assume a specific block size.")
  748. parser.add_argument(
  749. '--block-count',
  750. type=lambda x: int(x, 0),
  751. help="Assume a specific block count.")
  752. parser.add_argument(
  753. '-C', '--block-cycles',
  754. type=lambda x: int(x, 0),
  755. help="Assumed maximum number of erase cycles when measuring wear.")
  756. parser.add_argument(
  757. '-R', '--reset',
  758. action='store_true',
  759. help="Reset wear on block device initialization.")
  760. parser.add_argument(
  761. '--color',
  762. choices=['never', 'always', 'auto'],
  763. default='auto',
  764. help="When to use terminal colors. Defaults to 'auto'.")
  765. parser.add_argument(
  766. '--subscripts',
  767. action='store_true',
  768. help="Use unicode subscripts for showing wear.")
  769. parser.add_argument(
  770. '-:', '--dots',
  771. action='store_true',
  772. help="Use 1x2 ascii dot characters.")
  773. parser.add_argument(
  774. '-⣿', '--braille',
  775. action='store_true',
  776. help="Use 2x4 unicode braille characters. Note that braille characters "
  777. "sometimes suffer from inconsistent widths.")
  778. parser.add_argument(
  779. '--chars',
  780. help="Characters to use for read, prog, erase, noop operations.")
  781. parser.add_argument(
  782. '--wear-chars',
  783. help="Characters to use for showing wear.")
  784. parser.add_argument(
  785. '--colors',
  786. type=lambda x: [x.strip() for x in x.split(',')],
  787. help="Colors to use for read, prog, erase, noop operations.")
  788. parser.add_argument(
  789. '--wear-colors',
  790. type=lambda x: [x.strip() for x in x.split(',')],
  791. help="Colors to use for showing wear.")
  792. parser.add_argument(
  793. '-W', '--width',
  794. nargs='?',
  795. type=lambda x: int(x, 0),
  796. const=0,
  797. help="Width in columns. 0 uses the terminal width. Defaults to "
  798. "min(terminal, 80).")
  799. parser.add_argument(
  800. '-H', '--height',
  801. nargs='?',
  802. type=lambda x: int(x, 0),
  803. const=0,
  804. help="Height in rows. 0 uses the terminal height. Defaults to 1.")
  805. parser.add_argument(
  806. '-n', '--lines',
  807. nargs='?',
  808. type=lambda x: int(x, 0),
  809. const=0,
  810. help="Show this many lines of history. 0 uses the terminal height. "
  811. "Defaults to 5.")
  812. parser.add_argument(
  813. '-z', '--cat',
  814. action='store_true',
  815. help="Pipe directly to stdout.")
  816. parser.add_argument(
  817. '-U', '--hilbert',
  818. action='store_true',
  819. help="Render as a space-filling Hilbert curve.")
  820. parser.add_argument(
  821. '-Z', '--lebesgue',
  822. action='store_true',
  823. help="Render as a space-filling Z-curve.")
  824. parser.add_argument(
  825. '-c', '--coalesce',
  826. type=lambda x: int(x, 0),
  827. help="Number of operations to coalesce together.")
  828. parser.add_argument(
  829. '-s', '--sleep',
  830. type=float,
  831. help="Time in seconds to sleep between reads, coalescing operations.")
  832. parser.add_argument(
  833. '-k', '--keep-open',
  834. action='store_true',
  835. help="Reopen the pipe on EOF, useful when multiple "
  836. "processes are writing.")
  837. sys.exit(main(**{k: v
  838. for k, v in vars(parser.parse_intermixed_args()).items()
  839. if v is not None}))