diff --git a/Lib/profiling/sampling/_format_utils.py b/Lib/profiling/sampling/_format_utils.py new file mode 100644 index 00000000000000..237a4f4186bf20 --- /dev/null +++ b/Lib/profiling/sampling/_format_utils.py @@ -0,0 +1,5 @@ +import locale + + +def fmt(value: int | float, decimals: int = 1) -> str: + return locale.format_string(f'%.{decimals}f', value, grouping=True) diff --git a/Lib/profiling/sampling/_heatmap_assets/heatmap.js b/Lib/profiling/sampling/_heatmap_assets/heatmap.js index 8ac4ef43e53b37..3869a07bd2586e 100644 --- a/Lib/profiling/sampling/_heatmap_assets/heatmap.js +++ b/Lib/profiling/sampling/_heatmap_assets/heatmap.js @@ -598,10 +598,12 @@ function populateBytecodePanel(panel, button) { else if (specPct >= 33) specClass = 'medium'; // Build specialization summary + const instruction_word = instructions.length === 1 ? 'instruction' : 'instructions'; + const sample_word = totalSamples === 1 ? 'sample' : 'samples'; let html = `
${specPct}% specialized - (${specializedCount}/${instructions.length} instructions, ${specializedSamples.toLocaleString()}/${totalSamples.toLocaleString()} samples) + (${specializedCount}/${instructions.length} ${instruction_word}, ${specializedSamples.toLocaleString()}/${totalSamples.toLocaleString()} ${sample_word})
`; html += '
' + diff --git a/Lib/profiling/sampling/cli.py b/Lib/profiling/sampling/cli.py index 554167e43f5ed8..aacec645c3479c 100644 --- a/Lib/profiling/sampling/cli.py +++ b/Lib/profiling/sampling/cli.py @@ -2,6 +2,7 @@ import argparse import importlib.util +import locale import os import selectors import socket @@ -634,6 +635,16 @@ def _validate_args(args, parser): def main(): """Main entry point for the CLI.""" + # Set locale for number formatting, restore on exit + old_locale = locale.setlocale(locale.LC_ALL, None) + locale.setlocale(locale.LC_ALL, "") + try: + _main() + finally: + locale.setlocale(locale.LC_ALL, old_locale) + + +def _main(): # Create the main parser parser = argparse.ArgumentParser( description=_HELP_DESCRIPTION, diff --git a/Lib/profiling/sampling/heatmap_collector.py b/Lib/profiling/sampling/heatmap_collector.py index 5b4c89283be08c..076b4f35d4ad64 100644 --- a/Lib/profiling/sampling/heatmap_collector.py +++ b/Lib/profiling/sampling/heatmap_collector.py @@ -5,6 +5,7 @@ import html import importlib.resources import json +import locale import math import os import platform @@ -15,6 +16,7 @@ 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 @@ -343,7 +345,7 @@ def render_hierarchical_html(self, trees: Dict[str, TreeNode]) -> str:
{icon} {type_names[module_type]} - ({tree.count} {file_word}, {tree.samples:,} {sample_word}) + ({tree.count} {file_word}, {tree.samples:n} {sample_word})
''' @@ -390,7 +392,7 @@ def _render_folder(self, node: TreeNode, name: str, level: int = 1) -> str: parts.append(f'{indent} ') parts.append(f'{indent} 📁 {html.escape(name)}') parts.append(f'{indent} ' - f'({node.count} {file_word}, {node.samples:,} {sample_word})') + f'({node.count} {file_word}, {node.samples:n} {sample_word})') parts.append(f'{indent}
') parts.append(f'{indent}