def render(self, console: Console, options: ConsoleOptions) -> None: width = self.width or options.max_width or console.width options = options.update_dimensions(width, None) style = console.get_style(self.style) renderable = self.renderable if self.padding: renderable = Padding(renderable, self.padding) self._lines[:] = console.render_lines(renderable, options, style=style) self.size = Size(width, len(self._lines)) self.page.emit_no_wait(PageUpdate(self.page))
def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult: render_options = options.update(width=options.max_width - 4) lines = console.render_lines(self.elements, render_options, style=self.style) style = self.style new_line = Segment("\n") padding = Segment("▌ ", style) for line in lines: yield padding yield from line yield new_line
def from_renderable( cls, console: Console, renderable: RenderableType, width: int, height: int, ) -> "LineCache": options = console.options.update_dimensions(width, height) lines = console.render_lines(renderable, options) return cls(lines)
def render_bullet(self, console: Console, options: ConsoleOptions) -> RenderResult: render_options = options.update(width=options.max_width - 3) lines = console.render_lines(self.elements, render_options, style=self.style) bullet_style = console.get_style("markdown.item.bullet", default="none") bullet = Segment(" • ", bullet_style) padding = Segment(" " * 3, bullet_style) new_line = Segment("\n") for first, line in loop_first(lines): yield bullet if first else padding yield from line yield new_line
def _render_horizontal(self, console: Console, options: ConsoleOptions) -> RenderResult: render_widths = ratio_resolve(options.max_width, self.children) renders = [ console.render_lines(child, options.update(width=render_width)) for child, render_width in zip(self.children, render_widths) ] new_line = Segment.line() for lines in zip(*renders): for line in lines: yield from line yield new_line
def _render_vertical(self, console: Console, options: ConsoleOptions) -> RenderResult: render_heights = ratio_resolve(options.height or console.height, self.children) renders = [ console.render_lines(child.renderable, options.update(height=render_height), new_lines=True) for child, render_height in zip(self.children, render_heights) ] for render in renders: for line in render: yield from line
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: render_options = options.update( height=options.height or self.height or options.size.height) if not self.children: for line in console.render_lines(self._renderable or "", render_options, new_lines=True): yield from line elif self.direction == "vertical": yield from self._render_vertical(console, render_options) elif self.direction == "horizontal": yield from self._render_horizontal(console, render_options)
def render_number( self, console: Console, options: ConsoleOptions, number: int, last_number: int ) -> RenderResult: number_width = len(str(last_number)) + 2 render_options = options.update(width=options.max_width - number_width) lines = console.render_lines(self.elements, render_options, style=self.style) number_style = console.get_style("markdown.item.number", default="none") new_line = Segment("\n") padding = Segment(" " * number_width, number_style) numeral = Segment(f"{number}".rjust(number_width - 1) + " ", number_style) for first, line in loop_first(lines): yield numeral if first else padding yield from line yield new_line
def test_format_table(response): table = format_table(response) assert isinstance(table, Table) def _get_id(value): return str( value.get('id') or value.get('record', {}).get('id') or value.get('taxon', {}).get('id') ) # Just make sure at least object IDs show up in the table console = Console() rendered = '\n'.join([str(line) for line in console.render_lines(table)]) if isinstance(response, list): assert all([_get_id(value) in rendered for value in response])
def test_render_size(): console = Console(width=63, height=46, legacy_windows=False) options = console.options.update_dimensions(80, 4) lines = console.render_lines(Panel("foo", title="Hello"), options=options) print(repr(lines)) expected = [ [ Segment("╭─", Style()), Segment( "────────────────────────────────── Hello ───────────────────────────────────" ), Segment("─╮", Style()), ], [ Segment("│", Style()), Segment(" ", Style()), Segment("foo"), Segment( " ", Style(), ), Segment(" ", Style()), Segment("│", Style()), ], [ Segment("│", Style()), Segment(" ", Style()), Segment( " ", Style(), ), Segment(" ", Style()), Segment("│", Style()), ], [ Segment( "╰──────────────────────────────────────────────────────────────────────────────╯", Style(), ) ], ] assert lines == expected
def output_profiles( self, stats: ScaleneStatistics, pid: int, profile_this_code: Callable[[Filename, LineNumber], bool], python_alias_dir_name: Filename, python_alias_dir: Filename, profile_memory: bool = True, reduced_profile: bool = False, ) -> bool: """Write the profile out.""" # Get the children's stats, if any. if not pid: stats.merge_stats(python_alias_dir_name) try: shutil.rmtree(python_alias_dir) except BaseException: pass current_max: float = stats.max_footprint # If we've collected any samples, dump them. if (not stats.total_cpu_samples and not stats.total_memory_malloc_samples and not stats.total_memory_free_samples): # Nothing to output. return False # Collect all instrumented filenames. all_instrumented_files: List[Filename] = list( set( list(stats.cpu_samples_python.keys()) + list(stats.cpu_samples_c.keys()) + list(stats.memory_free_samples.keys()) + list(stats.memory_malloc_samples.keys()))) if not all_instrumented_files: # We didn't collect samples in source files. return False title = Text() mem_usage_line: Union[Text, str] = "" growth_rate = 0.0 if profile_memory: samples = stats.memory_footprint_samples if len(samples.get()) > 0: # Output a sparkline as a summary of memory usage over time. _, _, spark_str = sparkline.generate( samples.get()[0:samples.len()], 0, current_max) # Compute growth rate (slope), between 0 and 1. if stats.allocation_velocity[1] > 0: growth_rate = (100.0 * stats.allocation_velocity[0] / stats.allocation_velocity[1]) # If memory used is > 1GB, use GB as the unit. if current_max > 1024: mem_usage_line = Text.assemble( "Memory usage: ", ((spark_str, "blue")), (" (max: %6.2fGB, growth rate: %3.0f%%)\n" % ((current_max / 1024), growth_rate)), ) else: # Otherwise, use MB. mem_usage_line = Text.assemble( "Memory usage: ", ((spark_str, "blue")), (" (max: %6.2fMB, growth rate: %3.0f%%)\n" % (current_max, growth_rate)), ) null = open("/dev/null", "w") # Get column width of the terminal and adjust to fit. # Note that Scalene works best with at least 132 columns. if self.html: column_width = 132 else: column_width = shutil.get_terminal_size().columns console = Console( width=column_width, record=True, force_terminal=True, file=null, ) # Build a list of files we will actually report on. report_files: List[Filename] = [] # Sort in descending order of CPU cycles, and then ascending order by filename for fname in sorted( all_instrumented_files, key=lambda f: (-(stats.cpu_samples[f]), f), ): fname = Filename(fname) try: percent_cpu_time = (100 * stats.cpu_samples[fname] / stats.total_cpu_samples) except ZeroDivisionError: percent_cpu_time = 0 # Ignore files responsible for less than some percent of execution time and fewer than a threshold # of mallocs. if (stats.malloc_samples[fname] < self.malloc_threshold and percent_cpu_time < self.cpu_percent_threshold): continue report_files.append(fname) # Don't actually output the profile if we are a child process. # Instead, write info to disk for the main process to collect. if pid: stats.output_stats(pid, python_alias_dir_name) return True for fname in report_files: # Print header. percent_cpu_time = (100 * stats.cpu_samples[fname] / stats.total_cpu_samples) new_title = mem_usage_line + ( "%s: %% of time = %6.2f%% out of %6.2fs." % (fname, percent_cpu_time, stats.elapsed_time)) # Only display total memory usage once. mem_usage_line = "" tbl = Table( box=box.MINIMAL_HEAVY_HEAD, title=new_title, collapse_padding=True, width=column_width - 1, ) tbl.add_column("Line", justify="right", no_wrap=True) tbl.add_column("Time %\nPython", no_wrap=True) tbl.add_column("Time %\nnative", no_wrap=True) tbl.add_column("Sys\n%", no_wrap=True) tbl.add_column("GPU\n%", no_wrap=True) other_columns_width = 0 # Size taken up by all columns BUT code if profile_memory: tbl.add_column("Mem %\nPython", no_wrap=True) tbl.add_column("Net\n(MB)", no_wrap=True) tbl.add_column("Memory usage\nover time / %", no_wrap=True) tbl.add_column("Copy\n(MB/s)", no_wrap=True) other_columns_width = 72 + 5 # GPU tbl.add_column( "\n" + fname, width=column_width - other_columns_width, no_wrap=True, ) else: other_columns_width = 36 + 5 # GPU tbl.add_column( "\n" + fname, width=column_width - other_columns_width, no_wrap=True, ) # Print out the the profile for the source, line by line. with open(fname, "r") as source_file: # We track whether we should put in ellipsis (for reduced profiles) # or not. did_print = True # did we print a profile line last time? code_lines = source_file.read() # Generate syntax highlighted version for the whole file, # which we will consume a line at a time. # See https://github.com/willmcgugan/rich/discussions/965#discussioncomment-314233 syntax_highlighted = None if self.html: syntax_highlighted = Syntax( code_lines, "python", theme="default", line_numbers=False, code_width=None, ) else: syntax_highlighted = Syntax( code_lines, "python", theme="vim", line_numbers=False, code_width=None, ) capture_console = Console( width=column_width - other_columns_width, force_terminal=True, ) formatted_lines = [ SyntaxLine(segments) for segments in capture_console.render_lines(syntax_highlighted) ] for line_no, line in enumerate(formatted_lines, start=1): old_did_print = did_print did_print = self.output_profile_line( fname, LineNumber(line_no), line, console, tbl, stats, profile_this_code, profile_memory=profile_memory, force_print=True, suppress_lineno_print=False, is_function_summary=False, reduced_profile=reduced_profile, ) if old_did_print and not did_print: # We are skipping lines, so add an ellipsis. tbl.add_row("...") old_did_print = did_print # Potentially print a function summary. fn_stats = stats.build_function_stats(fname) print_fn_summary = False for fn_name in fn_stats.cpu_samples_python: if fn_name == fname: continue print_fn_summary = True break if print_fn_summary: tbl.add_row(None, end_section=True) txt = Text.assemble("function summary", style="bold italic") if profile_memory: tbl.add_row("", "", "", "", "", "", "", "", "", txt) else: tbl.add_row("", "", "", "", "", txt) for fn_name in sorted( fn_stats.cpu_samples_python, key=lambda k: stats.firstline_map[k], ): if fn_name == fname: continue if self.html: syntax_highlighted = Syntax( fn_name, "python", theme="default", line_numbers=False, code_width=None, ) else: syntax_highlighted = Syntax( fn_name, "python", theme="vim", line_numbers=False, code_width=None, ) # force print, suppress line numbers self.output_profile_line( fn_name, LineNumber(1), syntax_highlighted, # type: ignore console, tbl, fn_stats, profile_this_code, profile_memory=profile_memory, force_print=True, suppress_lineno_print=True, is_function_summary=True, reduced_profile=reduced_profile, ) console.print(tbl) # Report top K lines (currently 5) in terms of net memory consumption. net_mallocs: Dict[LineNumber, float] = defaultdict(float) for line_no in stats.bytei_map[fname]: for bytecode_index in stats.bytei_map[fname][line_no]: net_mallocs[line_no] += (stats.memory_malloc_samples[fname] [line_no][bytecode_index] - stats.memory_free_samples[fname] [line_no][bytecode_index]) net_mallocs = OrderedDict( sorted(net_mallocs.items(), key=itemgetter(1), reverse=True)) if len(net_mallocs) > 0: console.print("Top net memory consumption, by line:") number = 1 for net_malloc_lineno in net_mallocs: if net_mallocs[net_malloc_lineno] <= 1: break if number > 5: break output_str = ("(" + str(number) + ") " + ("%5.0f" % (net_malloc_lineno)) + ": " + ("%5.0f" % (net_mallocs[net_malloc_lineno])) + " MB") console.print(output_str) number += 1 # Only report potential leaks if the allocation velocity (growth rate) is above some threshold # FIXME: fixed at 1% for now. # We only report potential leaks where the confidence interval is quite tight and includes 1. growth_rate_threshold = 0.01 leak_reporting_threshold = 0.05 leaks = [] if growth_rate / 100 > growth_rate_threshold: vec = list(stats.leak_score[fname].values()) keys = list(stats.leak_score[fname].keys()) for index, item in enumerate(stats.leak_score[fname].values()): # See https://en.wikipedia.org/wiki/Rule_of_succession frees = item[1] allocs = item[0] expected_leak = (frees + 1) / (frees + allocs + 2) if expected_leak <= leak_reporting_threshold: leaks.append(( keys[index], 1 - expected_leak, net_mallocs[keys[index]], )) if len(leaks) > 0: # Report in descending order by least likelihood for leak in sorted(leaks, key=itemgetter(1), reverse=True): output_str = ( "Possible memory leak identified at line " + str(leak[0]) + " (estimated likelihood: " + ("%3.0f" % (leak[1] * 100)) + "%" + ", velocity: " + ("%3.0f MB/s" % (leak[2] / stats.elapsed_time)) + ")") console.print(output_str) if self.html: # Write HTML file. md = Markdown( "generated by the [scalene](https://github.com/plasma-umass/scalene) profiler" ) console.print(md) if not self.output_file: self.output_file = "/dev/stdout" console.save_html(self.output_file, clear=False) else: if not self.output_file: # No output file specified: write to stdout. sys.stdout.write(console.export_text(styles=True)) else: # Don't output styles to text file. console.save_text(self.output_file, styles=False, clear=False) return True
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: transparent_background = self._get_base_style().transaprent_background code_width = ((options.max_width - self._numbers_column_width - 1) if self.code_width is None else self.code_width) code = textwrap.dedent(self.code) if self.dedent else self.code code = code.expandtabs(self.tab_size) text = self.highlight(code) text.remove_suffix("\n") text.expand_tabs(self.tab_size) if not self.line_numbers: # Simple case of just rendering text yield from console.render(text, options=options.update(width=code_width)) return lines = text.split("\n") line_offset = 0 if self.line_range: start_line, end_line = self.line_range line_offset = max(0, start_line - 1) lines = lines[line_offset:end_line] numbers_column_width = self._numbers_column_width render_options = options.update(width=code_width) ( background_style, number_style, highlight_number_style, ) = self._get_number_styles(console) highlight_line = self.highlight_lines.__contains__ _Segment = Segment padding = _Segment(" " * numbers_column_width + " ", background_style) new_line = _Segment("\n") line_pointer = "❱ " for line_no, line in enumerate(lines, self.start_line + line_offset): if self.word_wrap: wrapped_lines = console.render_lines( line, render_options, style=background_style, pad=not transparent_background, ) else: segments = list(line.render(console, end="")) if options.no_wrap: wrapped_lines = [segments] else: wrapped_lines = [ _Segment.adjust_line_length( segments, render_options.max_width, style=background_style, pad=not transparent_background, ) ] for first, wrapped_line in loop_first(wrapped_lines): if first: line_column = str(line_no).rjust(numbers_column_width - 2) + " " if highlight_line(line_no): yield _Segment(line_pointer, Style(color="red")) yield _Segment(line_column, highlight_number_style) else: yield _Segment(" ", highlight_number_style) yield _Segment(line_column, number_style) else: yield padding yield from wrapped_line yield new_line
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult: code_width = ((options.max_width - self._numbers_column_width - 1) if self.code_width is None else self.code_width) code = self.code if self.dedent: code = textwrap.dedent(code) text = self._highlight(self.lexer_name) if not self.line_numbers: if self.code_width is None: yield text else: yield from console.render( text, options=options.update(width=code_width)) return lines = text.split("\n") line_offset = 0 if self.line_range: start_line, end_line = self.line_range line_offset = max(0, start_line - 1) lines = lines[line_offset:end_line] numbers_column_width = self._numbers_column_width render_options = options.update(width=code_width) ( background_style, number_style, highlight_number_style, ) = self._get_number_styles(console) highlight_line = self.highlight_lines.__contains__ _Segment = Segment padding = _Segment(" " * numbers_column_width + " ", background_style) new_line = _Segment("\n") line_pointer = "❱ " for line_no, line in enumerate(lines, self.start_line + line_offset): if self.word_wrap: wrapped_lines = console.render_lines(line, render_options, style=background_style) else: segments = list(line.render(console, end="")) wrapped_lines = [ Segment.adjust_line_length(segments, render_options.max_width, style=background_style) ] for first, wrapped_line in loop_first(wrapped_lines): if first: line_column = str(line_no).rjust(numbers_column_width - 2) + " " if highlight_line(line_no): yield _Segment(line_pointer, number_style) yield _Segment( line_column, highlight_number_style, ) else: yield _Segment(" ", highlight_number_style) yield _Segment( line_column, number_style, ) else: yield padding yield from wrapped_line yield new_line
def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: transparent_background = self._get_base_style().transparent_background code_width = ( ( (options.max_width - self._numbers_column_width - 1) if self.line_numbers else options.max_width ) if self.code_width is None else self.code_width ) ends_on_nl = self.code.endswith("\n") code = self.code if ends_on_nl else self.code + "\n" code = textwrap.dedent(code) if self.dedent else code code = code.expandtabs(self.tab_size) text = self.highlight(code, self.line_range) ( background_style, number_style, highlight_number_style, ) = self._get_number_styles(console) if not self.line_numbers and not self.word_wrap and not self.line_range: if not ends_on_nl: text.remove_suffix("\n") # Simple case of just rendering text style = ( self._get_base_style() + self._theme.get_style_for_token(Comment) + Style(dim=True) + self.background_style ) if self.indent_guides and not options.ascii_only: text = text.with_indent_guides(self.tab_size, style=style) text.overflow = "crop" if style.transparent_background: yield from console.render( text, options=options.update(width=code_width) ) else: syntax_lines = console.render_lines( text, options.update(width=code_width, height=None, justify="left"), style=self.background_style, pad=True, new_lines=True, ) for syntax_line in syntax_lines: yield from syntax_line return start_line, end_line = self.line_range or (None, None) line_offset = 0 if start_line: line_offset = max(0, start_line - 1) lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl) if self.line_range: lines = lines[line_offset:end_line] if self.indent_guides and not options.ascii_only: style = ( self._get_base_style() + self._theme.get_style_for_token(Comment) + Style(dim=True) + self.background_style ) lines = ( Text("\n") .join(lines) .with_indent_guides(self.tab_size, style=style) .split("\n", allow_blank=True) ) numbers_column_width = self._numbers_column_width render_options = options.update(width=code_width) highlight_line = self.highlight_lines.__contains__ _Segment = Segment padding = _Segment(" " * numbers_column_width + " ", background_style) new_line = _Segment("\n") line_pointer = "> " if options.legacy_windows else "❱ " for line_no, line in enumerate(lines, self.start_line + line_offset): if self.word_wrap: wrapped_lines = console.render_lines( line, render_options.update(height=None, justify="left"), style=background_style, pad=not transparent_background, ) else: segments = list(line.render(console, end="")) if options.no_wrap: wrapped_lines = [segments] else: wrapped_lines = [ _Segment.adjust_line_length( segments, render_options.max_width, style=background_style, pad=not transparent_background, ) ] if self.line_numbers: for first, wrapped_line in loop_first(wrapped_lines): if first: line_column = str(line_no).rjust(numbers_column_width - 2) + " " if highlight_line(line_no): yield _Segment(line_pointer, Style(color="red")) yield _Segment(line_column, highlight_number_style) else: yield _Segment(" ", highlight_number_style) yield _Segment(line_column, number_style) else: yield padding yield from wrapped_line yield new_line else: for wrapped_line in wrapped_lines: yield from wrapped_line yield new_line
def __init__(self, console: Console, renderable: RenderableType, width: int, height: int) -> None: self.lines = console.render_lines( renderable, console.options.update_dimensions(width, height)) self.offset = Offset(0, 0)