Skip to content

Commit 9cfd9ef

Browse files
committed
Format numbers with current locale
1 parent 49b0b0b commit 9cfd9ef

File tree

3 files changed

+49
-47
lines changed

3 files changed

+49
-47
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import locale
2+
3+
locale.setlocale(locale.LC_ALL, "")
4+
5+
6+
def fmt(value: int | float, decimals: int = 1) -> str:
7+
return locale.format_string(f'%.{decimals}f', value, grouping=True)

Lib/profiling/sampling/heatmap_collector.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import html
66
import importlib.resources
77
import json
8+
import locale
89
import math
910
import os
1011
import platform
@@ -15,6 +16,7 @@
1516
from typing import Dict, List, Tuple
1617

1718
from ._css_utils import get_combined_css
19+
from ._format_utils import fmt
1820
from .collector import normalize_location, extract_lineno
1921
from .stack_collector import StackTraceCollector
2022

@@ -343,7 +345,7 @@ def render_hierarchical_html(self, trees: Dict[str, TreeNode]) -> str:
343345
<div class="type-header" onclick="toggleTypeSection(this)">
344346
<span class="type-icon">{icon}</span>
345347
<span class="type-title">{type_names[module_type]}</span>
346-
<span class="type-stats">({tree.count} {file_word}, {tree.samples:,} {sample_word})</span>
348+
<span class="type-stats">({tree.count} {file_word}, {tree.samples:n} {sample_word})</span>
347349
</div>
348350
<div class="type-content"{content_style}>
349351
'''
@@ -390,7 +392,7 @@ def _render_folder(self, node: TreeNode, name: str, level: int = 1) -> str:
390392
parts.append(f'{indent} <span class="folder-icon">▶</span>')
391393
parts.append(f'{indent} <span class="folder-name">📁 {html.escape(name)}</span>')
392394
parts.append(f'{indent} <span class="folder-stats">'
393-
f'({node.count} {file_word}, {node.samples:,} {sample_word})</span>')
395+
f'({node.count} {file_word}, {node.samples:n} {sample_word})</span>')
394396
parts.append(f'{indent} </div>')
395397
parts.append(f'{indent} <div class="folder-content" style="display: none;">')
396398

@@ -435,7 +437,7 @@ def _render_file_item(self, stat: FileStats, indent: str = '') -> str:
435437

436438
return (f'{indent}<div class="file-item">\n'
437439
f'{indent} <a href="{html_file}" class="file-link" title="{full_path}">📄 {module_name}</a>\n'
438-
f'{indent} <span class="file-samples">{stat.total_samples:,} sample{s}</span>\n'
440+
f'{indent} <span class="file-samples">{stat.total_samples:n} sample{s}</span>\n'
439441
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'
440442
f'{indent}</div>\n')
441443

@@ -826,7 +828,7 @@ def _generate_index_html(self, index_path: Path, file_stats: List[FileStats]):
826828
# Format error rate and missed samples with bar classes
827829
error_rate = self.stats.get('error_rate')
828830
if error_rate is not None:
829-
error_rate_str = f"{error_rate:.1f}%"
831+
error_rate_str = f"{fmt(error_rate)}%"
830832
error_rate_width = min(error_rate, 100)
831833
# Determine bar color class based on rate
832834
if error_rate < 5:
@@ -842,7 +844,7 @@ def _generate_index_html(self, index_path: Path, file_stats: List[FileStats]):
842844

843845
missed_samples = self.stats.get('missed_samples')
844846
if missed_samples is not None:
845-
missed_samples_str = f"{missed_samples:.1f}%"
847+
missed_samples_str = f"{fmt(missed_samples)}%"
846848
missed_samples_width = min(missed_samples, 100)
847849
if missed_samples < 5:
848850
missed_samples_class = "good"
@@ -861,10 +863,10 @@ def _generate_index_html(self, index_path: Path, file_stats: List[FileStats]):
861863
"<!-- INLINE_JS -->": f"<script>\n{self._template_loader.index_js}\n</script>",
862864
"<!-- PYTHON_LOGO -->": self._template_loader.logo_html,
863865
"<!-- PYTHON_VERSION -->": f"{sys.version_info.major}.{sys.version_info.minor}",
864-
"<!-- NUM_FILES -->": f"{len(file_stats):,}",
865-
"<!-- TOTAL_SAMPLES -->": f"{self._total_samples:,}",
866-
"<!-- DURATION -->": f"{self.stats.get('duration_sec', 0):,.1f}s",
867-
"<!-- SAMPLE_RATE -->": f"{self.stats.get('sample_rate', 0):,.1f}",
866+
"<!-- NUM_FILES -->": f"{len(file_stats):n}",
867+
"<!-- TOTAL_SAMPLES -->": f"{self._total_samples:n}",
868+
"<!-- DURATION -->": fmt(self.stats.get('duration_sec', 0)),
869+
"<!-- SAMPLE_RATE -->": fmt(self.stats.get('sample_rate', 0)),
868870
"<!-- ERROR_RATE -->": error_rate_str,
869871
"<!-- ERROR_RATE_WIDTH -->": str(error_rate_width),
870872
"<!-- ERROR_RATE_CLASS -->": error_rate_class,
@@ -908,12 +910,12 @@ def _generate_file_html(self, output_path: Path, filename: str,
908910
# Populate template
909911
replacements = {
910912
"<!-- FILENAME -->": html.escape(filename),
911-
"<!-- TOTAL_SAMPLES -->": f"{file_stat.total_samples:,}",
912-
"<!-- TOTAL_SELF_SAMPLES -->": f"{file_stat.total_self_samples:,}",
913-
"<!-- NUM_LINES -->": f"{file_stat.num_lines:,}",
914-
"<!-- PERCENTAGE -->": f"{file_stat.percentage:.2f}",
915-
"<!-- MAX_SAMPLES -->": f"{file_stat.max_samples:,}",
916-
"<!-- MAX_SELF_SAMPLES -->": f"{file_stat.max_self_samples:,}",
913+
"<!-- TOTAL_SAMPLES -->": f"{file_stat.total_samples:n}",
914+
"<!-- TOTAL_SELF_SAMPLES -->": f"{file_stat.total_self_samples:n}",
915+
"<!-- NUM_LINES -->": f"{file_stat.num_lines:n}",
916+
"<!-- PERCENTAGE -->": fmt(file_stat.percentage, 2),
917+
"<!-- MAX_SAMPLES -->": f"{file_stat.max_samples:n}",
918+
"<!-- MAX_SELF_SAMPLES -->": f"{file_stat.max_self_samples:n}",
917919
"<!-- CODE_LINES -->": ''.join(code_lines_html),
918920
"<!-- INLINE_CSS -->": f"<style>\n{self._template_loader.file_css}\n</style>",
919921
"<!-- INLINE_JS -->": f"<script>\n{self._template_loader.file_js}\n</script>",
@@ -950,9 +952,9 @@ def _build_line_html(self, line_num: int, line_content: str,
950952
else:
951953
self_intensity = 0
952954

953-
self_display = f"{self_samples:,}" if self_samples > 0 else ""
954-
cumulative_display = f"{cumulative_samples:,}"
955-
tooltip = f"Self: {self_samples:,}, Total: {cumulative_samples:,}"
955+
self_display = f"{self_samples:n}" if self_samples > 0 else ""
956+
cumulative_display = f"{cumulative_samples:n}"
957+
tooltip = f"Self: {self_samples:n}, Total: {cumulative_samples:n}"
956958
else:
957959
cumulative_intensity = 0
958960
self_intensity = 0
@@ -1197,7 +1199,7 @@ def _create_navigation_button(self, items_with_counts: List[Tuple[str, int, str,
11971199
file, line, func, count = valid_items[0]
11981200
target_html = self.file_index[file]
11991201
nav_data = json.dumps({'link': f"{target_html}#line-{line}", 'func': func})
1200-
title = f"Go to {btn_class}: {html.escape(func)} ({count:,} samples)"
1202+
title = f"Go to {btn_class}: {html.escape(func)} ({count:n} samples)"
12011203
return f'<button class="nav-btn {btn_class}" data-nav=\'{html.escape(nav_data)}\' title="{title}">{arrow}</button>'
12021204

12031205
# Multiple items - create menu
@@ -1212,5 +1214,5 @@ def _create_navigation_button(self, items_with_counts: List[Tuple[str, int, str,
12121214
for file, line, func, count in valid_items
12131215
]
12141216
items_json = html.escape(json.dumps(items_data))
1215-
title = f"{len(items_data)} {btn_class}s ({total_samples:,} samples)"
1217+
title = f"{len(items_data)} {btn_class}s ({total_samples:n} samples)"
12161218
return f'<button class="nav-btn {btn_class}" data-nav-multi=\'{items_json}\' title="{title}">{arrow}</button>'

Lib/profiling/sampling/sample.py

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
11
import _remote_debugging
22
import os
3-
import pstats
43
import statistics
54
import sys
65
import sysconfig
76
import time
87
from collections import deque
98
from _colorize import ANSIColors
109

11-
from .pstats_collector import PstatsCollector
12-
from .stack_collector import CollapsedStackCollector, FlamegraphCollector
13-
from .heatmap_collector import HeatmapCollector
14-
from .gecko_collector import GeckoCollector
1510
from .constants import (
1611
PROFILING_MODE_WALL,
17-
PROFILING_MODE_CPU,
18-
PROFILING_MODE_GIL,
1912
PROFILING_MODE_ALL,
20-
PROFILING_MODE_EXCEPTION,
2113
)
14+
from ._format_utils import fmt
2215
try:
2316
from .live_collector import LiveStatsCollector
2417
except ImportError:
@@ -135,9 +128,9 @@ def sample(self, collector, duration_sec=10, *, async_aware=False):
135128
# Don't print stats for live mode (curses is handling display)
136129
is_live_mode = LiveStatsCollector is not None and isinstance(collector, LiveStatsCollector)
137130
if not is_live_mode:
138-
print(f"Captured {num_samples:,} samples in {running_time:.2f} seconds")
139-
print(f"Sample rate: {sample_rate:,.2f} samples/sec")
140-
print(f"Error rate: {error_rate:.2f}%")
131+
print(f"Captured {num_samples:n} samples in {fmt(running_time, 2)} seconds")
132+
print(f"Sample rate: {fmt(sample_rate, 2)} samples/sec")
133+
print(f"Error rate: {fmt(error_rate, 2)}")
141134

142135
# Print unwinder stats if stats collection is enabled
143136
if self.collect_stats:
@@ -151,7 +144,7 @@ def sample(self, collector, duration_sec=10, *, async_aware=False):
151144
print(
152145
f"Warning: missed {expected_samples - num_samples} samples "
153146
f"from the expected total of {expected_samples} "
154-
f"({(expected_samples - num_samples) / expected_samples * 100:.2f}%)"
147+
f"({fmt((expected_samples - num_samples) / expected_samples * 100, 2)}%)"
155148
)
156149

157150
def _print_realtime_stats(self):
@@ -185,16 +178,16 @@ def _print_realtime_stats(self):
185178
total = hits + partial + misses
186179
if total > 0:
187180
hit_pct = (hits + partial) / total * 100
188-
cache_stats_str = f" {ANSIColors.MAGENTA}Cache: {hit_pct:.1f}% ({hits}+{partial}/{misses}){ANSIColors.RESET}"
181+
cache_stats_str = f" {ANSIColors.MAGENTA}Cache: {fmt(hit_pct)}% ({hits}+{partial}/{misses}){ANSIColors.RESET}"
189182
except RuntimeError:
190183
pass
191184

192185
# Clear line and print stats
193186
print(
194187
f"\r\033[K{ANSIColors.BOLD_BLUE}Stats:{ANSIColors.RESET} "
195-
f"{ANSIColors.YELLOW}{mean_hz:.1f}Hz ({mean_us_per_sample:.1f}µs){ANSIColors.RESET} "
196-
f"{ANSIColors.GREEN}Min: {min_hz:.1f}Hz{ANSIColors.RESET} "
197-
f"{ANSIColors.RED}Max: {max_hz:.1f}Hz{ANSIColors.RESET} "
188+
f"{ANSIColors.YELLOW}{fmt(mean_hz)}Hz ({fmt(mean_us_per_sample)}µs){ANSIColors.RESET} "
189+
f"{ANSIColors.GREEN}Min: {fmt(min_hz)}Hz{ANSIColors.RESET} "
190+
f"{ANSIColors.RED}Max: {fmt(max_hz)}Hz{ANSIColors.RESET} "
198191
f"{ANSIColors.CYAN}N={self.total_samples}{ANSIColors.RESET}"
199192
f"{cache_stats_str}",
200193
end="",
@@ -224,10 +217,10 @@ def _print_unwinder_stats(self):
224217
misses_pct = (frame_cache_misses / total_lookups * 100) if total_lookups > 0 else 0
225218

226219
print(f" {ANSIColors.CYAN}Frame Cache:{ANSIColors.RESET}")
227-
print(f" Total samples: {total_samples:,}")
228-
print(f" Full hits: {frame_cache_hits:,} ({ANSIColors.GREEN}{hits_pct:.1f}%{ANSIColors.RESET})")
229-
print(f" Partial hits: {frame_cache_partial_hits:,} ({ANSIColors.YELLOW}{partial_pct:.1f}%{ANSIColors.RESET})")
230-
print(f" Misses: {frame_cache_misses:,} ({ANSIColors.RED}{misses_pct:.1f}%{ANSIColors.RESET})")
220+
print(f" Total samples: {total_samples:n}")
221+
print(f" Full hits: {frame_cache_hits:n} ({ANSIColors.GREEN}{fmt(hits_pct)}%{ANSIColors.RESET})")
222+
print(f" Partial hits: {frame_cache_partial_hits:n} ({ANSIColors.YELLOW}{fmt(partial_pct)}%{ANSIColors.RESET})")
223+
print(f" Misses: {frame_cache_misses:n} ({ANSIColors.RED}{fmt(misses_pct)}%{ANSIColors.RESET})")
231224

232225
# Frame read stats
233226
frames_from_cache = stats.get('frames_read_from_cache', 0)
@@ -237,8 +230,8 @@ def _print_unwinder_stats(self):
237230
memory_frame_pct = (frames_from_memory / total_frames * 100) if total_frames > 0 else 0
238231

239232
print(f" {ANSIColors.CYAN}Frame Reads:{ANSIColors.RESET}")
240-
print(f" From cache: {frames_from_cache:,} ({ANSIColors.GREEN}{cache_frame_pct:.1f}%{ANSIColors.RESET})")
241-
print(f" From memory: {frames_from_memory:,} ({ANSIColors.RED}{memory_frame_pct:.1f}%{ANSIColors.RESET})")
233+
print(f" From cache: {frames_from_cache:n} ({ANSIColors.GREEN}{fmt(cache_frame_pct)}%{ANSIColors.RESET})")
234+
print(f" From memory: {frames_from_memory:n} ({ANSIColors.RED}{fmt(memory_frame_pct)}%{ANSIColors.RESET})")
242235

243236
# Code object cache stats
244237
code_hits = stats.get('code_object_cache_hits', 0)
@@ -248,20 +241,20 @@ def _print_unwinder_stats(self):
248241
code_misses_pct = (code_misses / total_code * 100) if total_code > 0 else 0
249242

250243
print(f" {ANSIColors.CYAN}Code Object Cache:{ANSIColors.RESET}")
251-
print(f" Hits: {code_hits:,} ({ANSIColors.GREEN}{code_hits_pct:.1f}%{ANSIColors.RESET})")
252-
print(f" Misses: {code_misses:,} ({ANSIColors.RED}{code_misses_pct:.1f}%{ANSIColors.RESET})")
244+
print(f" Hits: {code_hits:n} ({ANSIColors.GREEN}{fmt(code_hits_pct)}%{ANSIColors.RESET})")
245+
print(f" Misses: {code_misses:n} ({ANSIColors.RED}{fmt(code_misses_pct)}%{ANSIColors.RESET})")
253246

254247
# Memory operations
255248
memory_reads = stats.get('memory_reads', 0)
256249
memory_bytes = stats.get('memory_bytes_read', 0)
257250
if memory_bytes >= 1024 * 1024:
258-
memory_str = f"{memory_bytes / (1024 * 1024):.1f} MB"
251+
memory_str = f"{fmt(memory_bytes / (1024 * 1024))} MB"
259252
elif memory_bytes >= 1024:
260-
memory_str = f"{memory_bytes / 1024:.1f} KB"
253+
memory_str = f"{fmt(memory_bytes / 1024)} KB"
261254
else:
262255
memory_str = f"{memory_bytes} B"
263256
print(f" {ANSIColors.CYAN}Memory:{ANSIColors.RESET}")
264-
print(f" Read operations: {memory_reads:,} ({memory_str})")
257+
print(f" Read operations: {memory_reads:n} ({memory_str})")
265258

266259
# Stale invalidations
267260
stale_invalidations = stats.get('stale_cache_invalidations', 0)

0 commit comments

Comments
 (0)