import html
import importlib.resources
import json
+import locale
import math
import os
import platform
from typing import Dict, List, Tuple
from ._css_utils import get_combined_css
+from ._format_utils import fmt
from .collector import normalize_location, extract_lineno
from .stack_collector import StackTraceCollector
<div class="type-header" onclick="toggleTypeSection(this)">
<span class="type-icon">{icon}</span>
<span class="type-title">{type_names[module_type]}</span>
- <span class="type-stats">({tree.count} {file_word}, {tree.samples:,} {sample_word})</span>
+ <span class="type-stats">({tree.count} {file_word}, {tree.samples:n} {sample_word})</span>
</div>
<div class="type-content"{content_style}>
'''
parts.append(f'{indent} <span class="folder-icon">▶</span>')
parts.append(f'{indent} <span class="folder-name">📁 {html.escape(name)}</span>')
parts.append(f'{indent} <span class="folder-stats">'
- f'({node.count} {file_word}, {node.samples:,} {sample_word})</span>')
+ f'({node.count} {file_word}, {node.samples:n} {sample_word})</span>')
parts.append(f'{indent} </div>')
parts.append(f'{indent} <div class="folder-content" style="display: none;">')
bar_width = min(stat.percentage, 100)
html_file = self.file_index[stat.filename]
+ s = "" if stat.total_samples == 1 else "s"
return (f'{indent}<div class="file-item">\n'
f'{indent} <a href="{html_file}" class="file-link" title="{full_path}">📄 {module_name}</a>\n'
- f'{indent} <span class="file-samples">{stat.total_samples:,} samples</span>\n'
+ f'{indent} <span class="file-samples">{stat.total_samples:n} sample{s}</span>\n'
f'{indent} <div class="heatmap-bar-container"><div class="heatmap-bar" style="width: {bar_width}px; height: {self.heatmap_bar_height}px;" data-intensity="{intensity:.3f}"></div></div>\n'
f'{indent}</div>\n')
"""Print summary of exported heatmap."""
print(f"Heatmap output written to {output_dir}/")
print(f" - Index: {output_dir / 'index.html'}")
- print(f" - {len(file_stats)} source file(s) analyzed")
+ s = "" if len(file_stats) == 1 else "s"
+ print(f" - {len(file_stats)} source file{s} analyzed")
def _calculate_file_stats(self) -> List[FileStats]:
"""Calculate statistics for each file.
# Format error rate and missed samples with bar classes
error_rate = self.stats.get('error_rate')
if error_rate is not None:
- error_rate_str = f"{error_rate:.1f}%"
+ error_rate_str = f"{fmt(error_rate)}%"
error_rate_width = min(error_rate, 100)
# Determine bar color class based on rate
if error_rate < 5:
missed_samples = self.stats.get('missed_samples')
if missed_samples is not None:
- missed_samples_str = f"{missed_samples:.1f}%"
+ missed_samples_str = f"{fmt(missed_samples)}%"
missed_samples_width = min(missed_samples, 100)
if missed_samples < 5:
missed_samples_class = "good"
"<!-- INLINE_JS -->": f"<script>\n{self._template_loader.index_js}\n</script>",
"<!-- PYTHON_LOGO -->": self._template_loader.logo_html,
"<!-- PYTHON_VERSION -->": f"{sys.version_info.major}.{sys.version_info.minor}",
- "<!-- NUM_FILES -->": str(len(file_stats)),
- "<!-- TOTAL_SAMPLES -->": f"{self._total_samples:,}",
- "<!-- DURATION -->": f"{self.stats.get('duration_sec', 0):.1f}s",
- "<!-- SAMPLE_RATE -->": f"{self.stats.get('sample_rate', 0):.1f}",
+ "<!-- NUM_FILES -->": f"{len(file_stats):n}",
+ "<!-- TOTAL_SAMPLES -->": f"{self._total_samples:n}",
+ "<!-- DURATION -->": fmt(self.stats.get('duration_sec', 0)),
+ "<!-- SAMPLE_RATE -->": fmt(self.stats.get('sample_rate', 0)),
"<!-- ERROR_RATE -->": error_rate_str,
"<!-- ERROR_RATE_WIDTH -->": str(error_rate_width),
"<!-- ERROR_RATE_CLASS -->": error_rate_class,
# Populate template
replacements = {
"<!-- FILENAME -->": html.escape(filename),
- "<!-- TOTAL_SAMPLES -->": f"{file_stat.total_samples:,}",
- "<!-- TOTAL_SELF_SAMPLES -->": f"{file_stat.total_self_samples:,}",
- "<!-- NUM_LINES -->": str(file_stat.num_lines),
- "<!-- PERCENTAGE -->": f"{file_stat.percentage:.2f}",
- "<!-- MAX_SAMPLES -->": str(file_stat.max_samples),
- "<!-- MAX_SELF_SAMPLES -->": str(file_stat.max_self_samples),
+ "<!-- TOTAL_SAMPLES -->": f"{file_stat.total_samples:n}",
+ "<!-- TOTAL_SELF_SAMPLES -->": f"{file_stat.total_self_samples:n}",
+ "<!-- NUM_LINES -->": f"{file_stat.num_lines:n}",
+ "<!-- PERCENTAGE -->": fmt(file_stat.percentage, 2),
+ "<!-- MAX_SAMPLES -->": f"{file_stat.max_samples:n}",
+ "<!-- MAX_SELF_SAMPLES -->": f"{file_stat.max_self_samples:n}",
"<!-- CODE_LINES -->": ''.join(code_lines_html),
"<!-- INLINE_CSS -->": f"<style>\n{self._template_loader.file_css}\n</style>",
"<!-- INLINE_JS -->": f"<script>\n{self._template_loader.file_js}\n</script>",
else:
self_intensity = 0
- self_display = f"{self_samples:,}" if self_samples > 0 else ""
- cumulative_display = f"{cumulative_samples:,}"
- tooltip = f"Self: {self_samples:,}, Total: {cumulative_samples:,}"
+ self_display = f"{self_samples:n}" if self_samples > 0 else ""
+ cumulative_display = f"{cumulative_samples:n}"
+ tooltip = f"Self: {self_samples:n}, Total: {cumulative_samples:n}"
else:
cumulative_intensity = 0
self_intensity = 0
file, line, func, count = valid_items[0]
target_html = self.file_index[file]
nav_data = json.dumps({'link': f"{target_html}#line-{line}", 'func': func})
- title = f"Go to {btn_class}: {html.escape(func)} ({count:,} samples)"
+ title = f"Go to {btn_class}: {html.escape(func)} ({count:n} samples)"
return f'<button class="nav-btn {btn_class}" data-nav=\'{html.escape(nav_data)}\' title="{title}">{arrow}</button>'
# Multiple items - create menu
for file, line, func, count in valid_items
]
items_json = html.escape(json.dumps(items_data))
- title = f"{len(items_data)} {btn_class}s ({total_samples:,} samples)"
+ title = f"{len(items_data)} {btn_class}s ({total_samples:n} samples)"
return f'<button class="nav-btn {btn_class}" data-nav-multi=\'{items_json}\' title="{title}">{arrow}</button>'
import _remote_debugging
import os
-import pstats
import statistics
import sys
import sysconfig
from collections import deque
from _colorize import ANSIColors
-from .pstats_collector import PstatsCollector
-from .stack_collector import CollapsedStackCollector, FlamegraphCollector
-from .heatmap_collector import HeatmapCollector
-from .gecko_collector import GeckoCollector
from .constants import (
PROFILING_MODE_WALL,
PROFILING_MODE_CPU,
PROFILING_MODE_ALL,
PROFILING_MODE_EXCEPTION,
)
+from ._format_utils import fmt
try:
from .live_collector import LiveStatsCollector
except ImportError:
# Don't print stats for live mode (curses is handling display)
is_live_mode = LiveStatsCollector is not None and isinstance(collector, LiveStatsCollector)
if not is_live_mode:
- print(f"Captured {num_samples} samples in {running_time:.2f} seconds")
- print(f"Sample rate: {sample_rate:.2f} samples/sec")
- print(f"Error rate: {error_rate:.2f}%")
+ print(f"Captured {num_samples:n} samples in {fmt(running_time, 2)} seconds")
+ print(f"Sample rate: {fmt(sample_rate, 2)} samples/sec")
+ print(f"Error rate: {fmt(error_rate, 2)}")
# Print unwinder stats if stats collection is enabled
if self.collect_stats:
print(
f"Warning: missed {expected_samples - num_samples} samples "
f"from the expected total of {expected_samples} "
- f"({(expected_samples - num_samples) / expected_samples * 100:.2f}%)"
+ f"({fmt((expected_samples - num_samples) / expected_samples * 100, 2)}%)"
)
def _print_realtime_stats(self):
total = hits + partial + misses
if total > 0:
hit_pct = (hits + partial) / total * 100
- cache_stats_str = f" {ANSIColors.MAGENTA}Cache: {hit_pct:.1f}% ({hits}+{partial}/{misses}){ANSIColors.RESET}"
+ cache_stats_str = f" {ANSIColors.MAGENTA}Cache: {fmt(hit_pct)}% ({hits}+{partial}/{misses}){ANSIColors.RESET}"
except RuntimeError:
pass
# Clear line and print stats
print(
f"\r\033[K{ANSIColors.BOLD_BLUE}Stats:{ANSIColors.RESET} "
- f"{ANSIColors.YELLOW}{mean_hz:.1f}Hz ({mean_us_per_sample:.1f}µs){ANSIColors.RESET} "
- f"{ANSIColors.GREEN}Min: {min_hz:.1f}Hz{ANSIColors.RESET} "
- f"{ANSIColors.RED}Max: {max_hz:.1f}Hz{ANSIColors.RESET} "
+ f"{ANSIColors.YELLOW}{fmt(mean_hz)}Hz ({fmt(mean_us_per_sample)}µs){ANSIColors.RESET} "
+ f"{ANSIColors.GREEN}Min: {fmt(min_hz)}Hz{ANSIColors.RESET} "
+ f"{ANSIColors.RED}Max: {fmt(max_hz)}Hz{ANSIColors.RESET} "
f"{ANSIColors.CYAN}N={self.total_samples}{ANSIColors.RESET}"
f"{cache_stats_str}",
end="",
misses_pct = (frame_cache_misses / total_lookups * 100) if total_lookups > 0 else 0
print(f" {ANSIColors.CYAN}Frame Cache:{ANSIColors.RESET}")
- print(f" Total samples: {total_samples:,}")
- print(f" Full hits: {frame_cache_hits:,} ({ANSIColors.GREEN}{hits_pct:.1f}%{ANSIColors.RESET})")
- print(f" Partial hits: {frame_cache_partial_hits:,} ({ANSIColors.YELLOW}{partial_pct:.1f}%{ANSIColors.RESET})")
- print(f" Misses: {frame_cache_misses:,} ({ANSIColors.RED}{misses_pct:.1f}%{ANSIColors.RESET})")
+ print(f" Total samples: {total_samples:n}")
+ print(f" Full hits: {frame_cache_hits:n} ({ANSIColors.GREEN}{fmt(hits_pct)}%{ANSIColors.RESET})")
+ print(f" Partial hits: {frame_cache_partial_hits:n} ({ANSIColors.YELLOW}{fmt(partial_pct)}%{ANSIColors.RESET})")
+ print(f" Misses: {frame_cache_misses:n} ({ANSIColors.RED}{fmt(misses_pct)}%{ANSIColors.RESET})")
# Frame read stats
frames_from_cache = stats.get('frames_read_from_cache', 0)
memory_frame_pct = (frames_from_memory / total_frames * 100) if total_frames > 0 else 0
print(f" {ANSIColors.CYAN}Frame Reads:{ANSIColors.RESET}")
- print(f" From cache: {frames_from_cache:,} ({ANSIColors.GREEN}{cache_frame_pct:.1f}%{ANSIColors.RESET})")
- print(f" From memory: {frames_from_memory:,} ({ANSIColors.RED}{memory_frame_pct:.1f}%{ANSIColors.RESET})")
+ print(f" From cache: {frames_from_cache:n} ({ANSIColors.GREEN}{fmt(cache_frame_pct)}%{ANSIColors.RESET})")
+ print(f" From memory: {frames_from_memory:n} ({ANSIColors.RED}{fmt(memory_frame_pct)}%{ANSIColors.RESET})")
# Code object cache stats
code_hits = stats.get('code_object_cache_hits', 0)
code_misses_pct = (code_misses / total_code * 100) if total_code > 0 else 0
print(f" {ANSIColors.CYAN}Code Object Cache:{ANSIColors.RESET}")
- print(f" Hits: {code_hits:,} ({ANSIColors.GREEN}{code_hits_pct:.1f}%{ANSIColors.RESET})")
- print(f" Misses: {code_misses:,} ({ANSIColors.RED}{code_misses_pct:.1f}%{ANSIColors.RESET})")
+ print(f" Hits: {code_hits:n} ({ANSIColors.GREEN}{fmt(code_hits_pct)}%{ANSIColors.RESET})")
+ print(f" Misses: {code_misses:n} ({ANSIColors.RED}{fmt(code_misses_pct)}%{ANSIColors.RESET})")
# Memory operations
memory_reads = stats.get('memory_reads', 0)
memory_bytes = stats.get('memory_bytes_read', 0)
if memory_bytes >= 1024 * 1024:
- memory_str = f"{memory_bytes / (1024 * 1024):.1f} MB"
+ memory_str = f"{fmt(memory_bytes / (1024 * 1024))} MB"
elif memory_bytes >= 1024:
- memory_str = f"{memory_bytes / 1024:.1f} KB"
+ memory_str = f"{fmt(memory_bytes / 1024)} KB"
else:
memory_str = f"{memory_bytes} B"
print(f" {ANSIColors.CYAN}Memory:{ANSIColors.RESET}")
- print(f" Read operations: {memory_reads:,} ({memory_str})")
+ print(f" Read operations: {memory_reads:n} ({memory_str})")
# Stale invalidations
stale_invalidations = stats.get('stale_cache_invalidations', 0)