tracebd.py 30 KB

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