1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0-only 3# Copyright (C) 2024 ARM Ltd. 4# 5# Utility providing smaps-like output detailing transparent hugepage usage. 6# For more info, run: 7# ./thpmaps --help 8# 9# Requires numpy: 10# pip3 install numpy 11 12 13import argparse 14import collections 15import math 16import os 17import re 18import resource 19import shutil 20import sys 21import textwrap 22import time 23import numpy as np 24 25 26with open('/sys/kernel/mm/transparent_hugepage/hpage_pmd_size') as f: 27 PAGE_SIZE = resource.getpagesize() 28 PAGE_SHIFT = int(math.log2(PAGE_SIZE)) 29 PMD_SIZE = int(f.read()) 30 PMD_ORDER = int(math.log2(PMD_SIZE / PAGE_SIZE)) 31 32 33def align_forward(v, a): 34 return (v + (a - 1)) & ~(a - 1) 35 36 37def align_offset(v, a): 38 return v & (a - 1) 39 40 41def kbnr(kb): 42 # Convert KB to number of pages. 43 return (kb << 10) >> PAGE_SHIFT 44 45 46def nrkb(nr): 47 # Convert number of pages to KB. 48 return (nr << PAGE_SHIFT) >> 10 49 50 51def odkb(order): 52 # Convert page order to KB. 53 return (PAGE_SIZE << order) >> 10 54 55 56def cont_ranges_all(search, index): 57 # Given a list of arrays, find the ranges for which values are monotonically 58 # incrementing in all arrays. all arrays in search and index must be the 59 # same size. 60 sz = len(search[0]) 61 r = np.full(sz, 2) 62 d = np.diff(search[0]) == 1 63 for dd in [np.diff(arr) == 1 for arr in search[1:]]: 64 d &= dd 65 r[1:] -= d 66 r[:-1] -= d 67 return [np.repeat(arr, r).reshape(-1, 2) for arr in index] 68 69 70class ArgException(Exception): 71 pass 72 73 74class FileIOException(Exception): 75 pass 76 77 78class BinArrayFile: 79 # Base class used to read /proc/<pid>/pagemap and /proc/kpageflags into a 80 # numpy array. Use inherrited class in a with clause to ensure file is 81 # closed when it goes out of scope. 82 def __init__(self, filename, element_size): 83 self.element_size = element_size 84 self.filename = filename 85 self.fd = os.open(self.filename, os.O_RDONLY) 86 87 def cleanup(self): 88 os.close(self.fd) 89 90 def __enter__(self): 91 return self 92 93 def __exit__(self, exc_type, exc_val, exc_tb): 94 self.cleanup() 95 96 def _readin(self, offset, buffer): 97 length = os.preadv(self.fd, (buffer,), offset) 98 if len(buffer) != length: 99 raise FileIOException('error: {} failed to read {} bytes at {:x}' 100 .format(self.filename, len(buffer), offset)) 101 102 def _toarray(self, buf): 103 assert(self.element_size == 8) 104 return np.frombuffer(buf, dtype=np.uint64) 105 106 def getv(self, vec): 107 vec *= self.element_size 108 offsets = vec[:, 0] 109 lengths = (np.diff(vec) + self.element_size).reshape(len(vec)) 110 buf = bytearray(int(np.sum(lengths))) 111 view = memoryview(buf) 112 pos = 0 113 for offset, length in zip(offsets, lengths): 114 offset = int(offset) 115 length = int(length) 116 self._readin(offset, view[pos:pos+length]) 117 pos += length 118 return self._toarray(buf) 119 120 def get(self, index, nr=1): 121 offset = index * self.element_size 122 length = nr * self.element_size 123 buf = bytearray(length) 124 self._readin(offset, buf) 125 return self._toarray(buf) 126 127 128PM_PAGE_PRESENT = 1 << 63 129PM_PFN_MASK = (1 << 55) - 1 130 131class PageMap(BinArrayFile): 132 # Read ranges of a given pid's pagemap into a numpy array. 133 def __init__(self, pid='self'): 134 super().__init__(f'/proc/{pid}/pagemap', 8) 135 136 137KPF_ANON = 1 << 12 138KPF_COMPOUND_HEAD = 1 << 15 139KPF_COMPOUND_TAIL = 1 << 16 140KPF_THP = 1 << 22 141 142class KPageFlags(BinArrayFile): 143 # Read ranges of /proc/kpageflags into a numpy array. 144 def __init__(self): 145 super().__init__(f'/proc/kpageflags', 8) 146 147 148vma_all_stats = set([ 149 "Size", 150 "Rss", 151 "Pss", 152 "Pss_Dirty", 153 "Shared_Clean", 154 "Shared_Dirty", 155 "Private_Clean", 156 "Private_Dirty", 157 "Referenced", 158 "Anonymous", 159 "KSM", 160 "LazyFree", 161 "AnonHugePages", 162 "ShmemPmdMapped", 163 "FilePmdMapped", 164 "Shared_Hugetlb", 165 "Private_Hugetlb", 166 "Swap", 167 "SwapPss", 168 "Locked", 169]) 170 171vma_min_stats = set([ 172 "Rss", 173 "Anonymous", 174 "AnonHugePages", 175 "ShmemPmdMapped", 176 "FilePmdMapped", 177]) 178 179VMA = collections.namedtuple('VMA', [ 180 'name', 181 'start', 182 'end', 183 'read', 184 'write', 185 'execute', 186 'private', 187 'pgoff', 188 'major', 189 'minor', 190 'inode', 191 'stats', 192]) 193 194class VMAList: 195 # A container for VMAs, parsed from /proc/<pid>/smaps. Iterate over the 196 # instance to receive VMAs. 197 def __init__(self, pid='self', stats=[]): 198 self.vmas = [] 199 with open(f'/proc/{pid}/smaps', 'r') as file: 200 for line in file: 201 elements = line.split() 202 if '-' in elements[0]: 203 start, end = map(lambda x: int(x, 16), elements[0].split('-')) 204 major, minor = map(lambda x: int(x, 16), elements[3].split(':')) 205 self.vmas.append(VMA( 206 name=elements[5] if len(elements) == 6 else '', 207 start=start, 208 end=end, 209 read=elements[1][0] == 'r', 210 write=elements[1][1] == 'w', 211 execute=elements[1][2] == 'x', 212 private=elements[1][3] == 'p', 213 pgoff=int(elements[2], 16), 214 major=major, 215 minor=minor, 216 inode=int(elements[4], 16), 217 stats={}, 218 )) 219 else: 220 param = elements[0][:-1] 221 if param in stats: 222 value = int(elements[1]) 223 self.vmas[-1].stats[param] = {'type': None, 'value': value} 224 225 def __iter__(self): 226 yield from self.vmas 227 228 229def thp_parse(vma, kpageflags, ranges, indexes, vfns, pfns, anons, heads): 230 # Given 4 same-sized arrays representing a range within a page table backed 231 # by THPs (vfns: virtual frame numbers, pfns: physical frame numbers, anons: 232 # True if page is anonymous, heads: True if page is head of a THP), return a 233 # dictionary of statistics describing the mapped THPs. 234 stats = { 235 'file': { 236 'partial': 0, 237 'aligned': [0] * (PMD_ORDER + 1), 238 'unaligned': [0] * (PMD_ORDER + 1), 239 }, 240 'anon': { 241 'partial': 0, 242 'aligned': [0] * (PMD_ORDER + 1), 243 'unaligned': [0] * (PMD_ORDER + 1), 244 }, 245 } 246 247 for rindex, rpfn in zip(ranges[0], ranges[2]): 248 index_next = int(rindex[0]) 249 index_end = int(rindex[1]) + 1 250 pfn_end = int(rpfn[1]) + 1 251 252 folios = indexes[index_next:index_end][heads[index_next:index_end]] 253 254 # Account pages for any partially mapped THP at the front. In that case, 255 # the first page of the range is a tail. 256 nr = (int(folios[0]) if len(folios) else index_end) - index_next 257 stats['anon' if anons[index_next] else 'file']['partial'] += nr 258 259 # Account pages for any partially mapped THP at the back. In that case, 260 # the next page after the range is a tail. 261 if len(folios): 262 flags = int(kpageflags.get(pfn_end)[0]) 263 if flags & KPF_COMPOUND_TAIL: 264 nr = index_end - int(folios[-1]) 265 folios = folios[:-1] 266 index_end -= nr 267 stats['anon' if anons[index_end - 1] else 'file']['partial'] += nr 268 269 # Account fully mapped THPs in the middle of the range. 270 if len(folios): 271 folio_nrs = np.append(np.diff(folios), np.uint64(index_end - folios[-1])) 272 folio_orders = np.log2(folio_nrs).astype(np.uint64) 273 for index, order in zip(folios, folio_orders): 274 index = int(index) 275 order = int(order) 276 nr = 1 << order 277 vfn = int(vfns[index]) 278 align = 'aligned' if align_forward(vfn, nr) == vfn else 'unaligned' 279 anon = 'anon' if anons[index] else 'file' 280 stats[anon][align][order] += nr 281 282 # Account PMD-mapped THPs spearately, so filter out of the stats. There is a 283 # race between acquiring the smaps stats and reading pagemap, where memory 284 # could be deallocated. So clamp to zero incase it would have gone negative. 285 anon_pmd_mapped = vma.stats['AnonHugePages']['value'] 286 file_pmd_mapped = vma.stats['ShmemPmdMapped']['value'] + \ 287 vma.stats['FilePmdMapped']['value'] 288 stats['anon']['aligned'][PMD_ORDER] = max(0, stats['anon']['aligned'][PMD_ORDER] - kbnr(anon_pmd_mapped)) 289 stats['file']['aligned'][PMD_ORDER] = max(0, stats['file']['aligned'][PMD_ORDER] - kbnr(file_pmd_mapped)) 290 291 rstats = { 292 f"anon-thp-pmd-aligned-{odkb(PMD_ORDER)}kB": {'type': 'anon', 'value': anon_pmd_mapped}, 293 f"file-thp-pmd-aligned-{odkb(PMD_ORDER)}kB": {'type': 'file', 'value': file_pmd_mapped}, 294 } 295 296 def flatten_sub(type, subtype, stats): 297 param = f"{type}-thp-pte-{subtype}-{{}}kB" 298 for od, nr in enumerate(stats[2:], 2): 299 rstats[param.format(odkb(od))] = {'type': type, 'value': nrkb(nr)} 300 301 def flatten_type(type, stats): 302 flatten_sub(type, 'aligned', stats['aligned']) 303 flatten_sub(type, 'unaligned', stats['unaligned']) 304 rstats[f"{type}-thp-pte-partial"] = {'type': type, 'value': nrkb(stats['partial'])} 305 306 flatten_type('anon', stats['anon']) 307 flatten_type('file', stats['file']) 308 309 return rstats 310 311 312def cont_parse(vma, order, ranges, anons, heads): 313 # Given 4 same-sized arrays representing a range within a page table backed 314 # by THPs (vfns: virtual frame numbers, pfns: physical frame numbers, anons: 315 # True if page is anonymous, heads: True if page is head of a THP), return a 316 # dictionary of statistics describing the contiguous blocks. 317 nr_cont = 1 << order 318 nr_anon = 0 319 nr_file = 0 320 321 for rindex, rvfn, rpfn in zip(*ranges): 322 index_next = int(rindex[0]) 323 index_end = int(rindex[1]) + 1 324 vfn_start = int(rvfn[0]) 325 pfn_start = int(rpfn[0]) 326 327 if align_offset(pfn_start, nr_cont) != align_offset(vfn_start, nr_cont): 328 continue 329 330 off = align_forward(vfn_start, nr_cont) - vfn_start 331 index_next += off 332 333 while index_next + nr_cont <= index_end: 334 folio_boundary = heads[index_next+1:index_next+nr_cont].any() 335 if not folio_boundary: 336 if anons[index_next]: 337 nr_anon += nr_cont 338 else: 339 nr_file += nr_cont 340 index_next += nr_cont 341 342 # Account blocks that are PMD-mapped spearately, so filter out of the stats. 343 # There is a race between acquiring the smaps stats and reading pagemap, 344 # where memory could be deallocated. So clamp to zero incase it would have 345 # gone negative. 346 anon_pmd_mapped = vma.stats['AnonHugePages']['value'] 347 file_pmd_mapped = vma.stats['ShmemPmdMapped']['value'] + \ 348 vma.stats['FilePmdMapped']['value'] 349 nr_anon = max(0, nr_anon - kbnr(anon_pmd_mapped)) 350 nr_file = max(0, nr_file - kbnr(file_pmd_mapped)) 351 352 rstats = { 353 f"anon-cont-pmd-aligned-{nrkb(nr_cont)}kB": {'type': 'anon', 'value': anon_pmd_mapped}, 354 f"file-cont-pmd-aligned-{nrkb(nr_cont)}kB": {'type': 'file', 'value': file_pmd_mapped}, 355 } 356 357 rstats[f"anon-cont-pte-aligned-{nrkb(nr_cont)}kB"] = {'type': 'anon', 'value': nrkb(nr_anon)} 358 rstats[f"file-cont-pte-aligned-{nrkb(nr_cont)}kB"] = {'type': 'file', 'value': nrkb(nr_file)} 359 360 return rstats 361 362 363def vma_print(vma, pid): 364 # Prints a VMA instance in a format similar to smaps. The main difference is 365 # that the pid is included as the first value. 366 print("{:010d}: {:016x}-{:016x} {}{}{}{} {:08x} {:02x}:{:02x} {:08x} {}" 367 .format( 368 pid, vma.start, vma.end, 369 'r' if vma.read else '-', 'w' if vma.write else '-', 370 'x' if vma.execute else '-', 'p' if vma.private else 's', 371 vma.pgoff, vma.major, vma.minor, vma.inode, vma.name 372 )) 373 374 375def stats_print(stats, tot_anon, tot_file, inc_empty): 376 # Print a statistics dictionary. 377 label_field = 32 378 for label, stat in stats.items(): 379 type = stat['type'] 380 value = stat['value'] 381 if value or inc_empty: 382 pad = max(0, label_field - len(label) - 1) 383 if type == 'anon' and tot_anon > 0: 384 percent = f' ({value / tot_anon:3.0%})' 385 elif type == 'file' and tot_file > 0: 386 percent = f' ({value / tot_file:3.0%})' 387 else: 388 percent = '' 389 print(f"{label}:{' ' * pad}{value:8} kB{percent}") 390 391 392def vma_parse(vma, pagemap, kpageflags, contorders): 393 # Generate thp and cont statistics for a single VMA. 394 start = vma.start >> PAGE_SHIFT 395 end = vma.end >> PAGE_SHIFT 396 397 pmes = pagemap.get(start, end - start) 398 present = pmes & PM_PAGE_PRESENT != 0 399 pfns = pmes & PM_PFN_MASK 400 pfns = pfns[present] 401 vfns = np.arange(start, end, dtype=np.uint64) 402 vfns = vfns[present] 403 404 pfn_vec = cont_ranges_all([pfns], [pfns])[0] 405 flags = kpageflags.getv(pfn_vec) 406 anons = flags & KPF_ANON != 0 407 heads = flags & KPF_COMPOUND_HEAD != 0 408 thps = flags & KPF_THP != 0 409 410 vfns = vfns[thps] 411 pfns = pfns[thps] 412 anons = anons[thps] 413 heads = heads[thps] 414 415 indexes = np.arange(len(vfns), dtype=np.uint64) 416 ranges = cont_ranges_all([vfns, pfns], [indexes, vfns, pfns]) 417 418 thpstats = thp_parse(vma, kpageflags, ranges, indexes, vfns, pfns, anons, heads) 419 contstats = [cont_parse(vma, order, ranges, anons, heads) for order in contorders] 420 421 tot_anon = vma.stats['Anonymous']['value'] 422 tot_file = vma.stats['Rss']['value'] - tot_anon 423 424 return { 425 **thpstats, 426 **{k: v for s in contstats for k, v in s.items()} 427 }, tot_anon, tot_file 428 429 430def do_main(args): 431 pids = set() 432 rollup = {} 433 rollup_anon = 0 434 rollup_file = 0 435 436 if args.cgroup: 437 strict = False 438 for walk_info in os.walk(args.cgroup): 439 cgroup = walk_info[0] 440 with open(f'{cgroup}/cgroup.procs') as pidfile: 441 for line in pidfile.readlines(): 442 pids.add(int(line.strip())) 443 elif args.pid: 444 strict = True 445 pids = pids.union(args.pid) 446 else: 447 strict = False 448 for pid in os.listdir('/proc'): 449 if pid.isdigit(): 450 pids.add(int(pid)) 451 452 if not args.rollup: 453 print(" PID START END PROT OFFSET DEV INODE OBJECT") 454 455 for pid in pids: 456 try: 457 with PageMap(pid) as pagemap: 458 with KPageFlags() as kpageflags: 459 for vma in VMAList(pid, vma_all_stats if args.inc_smaps else vma_min_stats): 460 if (vma.read or vma.write or vma.execute) and vma.stats['Rss']['value'] > 0: 461 stats, vma_anon, vma_file = vma_parse(vma, pagemap, kpageflags, args.cont) 462 else: 463 stats = {} 464 vma_anon = 0 465 vma_file = 0 466 if args.inc_smaps: 467 stats = {**vma.stats, **stats} 468 if args.rollup: 469 for k, v in stats.items(): 470 if k in rollup: 471 assert(rollup[k]['type'] == v['type']) 472 rollup[k]['value'] += v['value'] 473 else: 474 rollup[k] = v 475 rollup_anon += vma_anon 476 rollup_file += vma_file 477 else: 478 vma_print(vma, pid) 479 stats_print(stats, vma_anon, vma_file, args.inc_empty) 480 except (FileNotFoundError, ProcessLookupError, FileIOException): 481 if strict: 482 raise 483 484 if args.rollup: 485 stats_print(rollup, rollup_anon, rollup_file, args.inc_empty) 486 487 488def main(): 489 docs_width = shutil.get_terminal_size().columns 490 docs_width -= 2 491 docs_width = min(80, docs_width) 492 493 def format(string): 494 text = re.sub(r'\s+', ' ', string) 495 text = re.sub(r'\s*\\n\s*', '\n', text) 496 paras = text.split('\n') 497 paras = [textwrap.fill(p, width=docs_width) for p in paras] 498 return '\n'.join(paras) 499 500 def formatter(prog): 501 return argparse.RawDescriptionHelpFormatter(prog, width=docs_width) 502 503 def size2order(human): 504 units = { 505 "K": 2**10, "M": 2**20, "G": 2**30, 506 "k": 2**10, "m": 2**20, "g": 2**30, 507 } 508 unit = 1 509 if human[-1] in units: 510 unit = units[human[-1]] 511 human = human[:-1] 512 try: 513 size = int(human) 514 except ValueError: 515 raise ArgException('error: --cont value must be integer size with optional KMG unit') 516 size *= unit 517 order = int(math.log2(size / PAGE_SIZE)) 518 if order < 1: 519 raise ArgException('error: --cont value must be size of at least 2 pages') 520 if (1 << order) * PAGE_SIZE != size: 521 raise ArgException('error: --cont value must be size of power-of-2 pages') 522 if order > PMD_ORDER: 523 raise ArgException('error: --cont value must be less than or equal to PMD order') 524 return order 525 526 parser = argparse.ArgumentParser(formatter_class=formatter, 527 description=format("""Prints information about how transparent huge 528 pages are mapped, either system-wide, or for a specified 529 process or cgroup.\\n 530 \\n 531 When run with --pid, the user explicitly specifies the set 532 of pids to scan. e.g. "--pid 10 [--pid 134 ...]". When run 533 with --cgroup, the user passes either a v1 or v2 cgroup and 534 all pids that belong to the cgroup subtree are scanned. When 535 run with neither --pid nor --cgroup, the full set of pids on 536 the system is gathered from /proc and scanned as if the user 537 had provided "--pid 1 --pid 2 ...".\\n 538 \\n 539 A default set of statistics is always generated for THP 540 mappings. However, it is also possible to generate 541 additional statistics for "contiguous block mappings" where 542 the block size is user-defined.\\n 543 \\n 544 Statistics are maintained independently for anonymous and 545 file-backed (pagecache) memory and are shown both in kB and 546 as a percentage of either total anonymous or total 547 file-backed memory as appropriate.\\n 548 \\n 549 THP Statistics\\n 550 --------------\\n 551 \\n 552 Statistics are always generated for fully- and 553 contiguously-mapped THPs whose mapping address is aligned to 554 their size, for each <size> supported by the system. 555 Separate counters describe THPs mapped by PTE vs those 556 mapped by PMD. (Although note a THP can only be mapped by 557 PMD if it is PMD-sized):\\n 558 \\n 559 - anon-thp-pte-aligned-<size>kB\\n 560 - file-thp-pte-aligned-<size>kB\\n 561 - anon-thp-pmd-aligned-<size>kB\\n 562 - file-thp-pmd-aligned-<size>kB\\n 563 \\n 564 Similarly, statistics are always generated for fully- and 565 contiguously-mapped THPs whose mapping address is *not* 566 aligned to their size, for each <size> supported by the 567 system. Due to the unaligned mapping, it is impossible to 568 map by PMD, so there are only PTE counters for this case:\\n 569 \\n 570 - anon-thp-pte-unaligned-<size>kB\\n 571 - file-thp-pte-unaligned-<size>kB\\n 572 \\n 573 Statistics are also always generated for mapped pages that 574 belong to a THP but where the is THP is *not* fully- and 575 contiguously- mapped. These "partial" mappings are all 576 counted in the same counter regardless of the size of the 577 THP that is partially mapped:\\n 578 \\n 579 - anon-thp-pte-partial\\n 580 - file-thp-pte-partial\\n 581 \\n 582 Contiguous Block Statistics\\n 583 ---------------------------\\n 584 \\n 585 An optional, additional set of statistics is generated for 586 every contiguous block size specified with `--cont <size>`. 587 These statistics show how much memory is mapped in 588 contiguous blocks of <size> and also aligned to <size>. A 589 given contiguous block must all belong to the same THP, but 590 there is no requirement for it to be the *whole* THP. 591 Separate counters describe contiguous blocks mapped by PTE 592 vs those mapped by PMD:\\n 593 \\n 594 - anon-cont-pte-aligned-<size>kB\\n 595 - file-cont-pte-aligned-<size>kB\\n 596 - anon-cont-pmd-aligned-<size>kB\\n 597 - file-cont-pmd-aligned-<size>kB\\n 598 \\n 599 As an example, if monitoring 64K contiguous blocks (--cont 600 64K), there are a number of sources that could provide such 601 blocks: a fully- and contiguously-mapped 64K THP that is 602 aligned to a 64K boundary would provide 1 block. A fully- 603 and contiguously-mapped 128K THP that is aligned to at least 604 a 64K boundary would provide 2 blocks. Or a 128K THP that 605 maps its first 100K, but contiguously and starting at a 64K 606 boundary would provide 1 block. A fully- and 607 contiguously-mapped 2M THP would provide 32 blocks. There 608 are many other possible permutations.\\n"""), 609 epilog=format("""Requires root privilege to access pagemap and 610 kpageflags.""")) 611 612 group = parser.add_mutually_exclusive_group(required=False) 613 group.add_argument('--pid', 614 metavar='pid', required=False, type=int, default=[], action='append', 615 help="""Process id of the target process. Maybe issued multiple times to 616 scan multiple processes. --pid and --cgroup are mutually exclusive. 617 If neither are provided, all processes are scanned to provide 618 system-wide information.""") 619 620 group.add_argument('--cgroup', 621 metavar='path', required=False, 622 help="""Path to the target cgroup in sysfs. Iterates over every pid in 623 the cgroup and its children. --pid and --cgroup are mutually 624 exclusive. If neither are provided, all processes are scanned to 625 provide system-wide information.""") 626 627 parser.add_argument('--rollup', 628 required=False, default=False, action='store_true', 629 help="""Sum the per-vma statistics to provide a summary over the whole 630 system, process or cgroup.""") 631 632 parser.add_argument('--cont', 633 metavar='size[KMG]', required=False, default=[], action='append', 634 help="""Adds stats for memory that is mapped in contiguous blocks of 635 <size> and also aligned to <size>. May be issued multiple times to 636 track multiple sized blocks. Useful to infer e.g. arm64 contpte and 637 hpa mappings. Size must be a power-of-2 number of pages.""") 638 639 parser.add_argument('--inc-smaps', 640 required=False, default=False, action='store_true', 641 help="""Include all numerical, additive /proc/<pid>/smaps stats in the 642 output.""") 643 644 parser.add_argument('--inc-empty', 645 required=False, default=False, action='store_true', 646 help="""Show all statistics including those whose value is 0.""") 647 648 parser.add_argument('--periodic', 649 metavar='sleep_ms', required=False, type=int, 650 help="""Run in a loop, polling every sleep_ms milliseconds.""") 651 652 args = parser.parse_args() 653 654 try: 655 args.cont = [size2order(cont) for cont in args.cont] 656 except ArgException as e: 657 parser.print_usage() 658 raise 659 660 if args.periodic: 661 while True: 662 do_main(args) 663 print() 664 time.sleep(args.periodic / 1000) 665 else: 666 do_main(args) 667 668 669if __name__ == "__main__": 670 try: 671 main() 672 except Exception as e: 673 prog = os.path.basename(sys.argv[0]) 674 print(f'{prog}: {e}') 675 exit(1) 676