Skip to content

Commit c6cbdb5

Browse files
committed
gh-153838: Skip non-regular and oversized files in heatmap exporter
1 parent abdd7ae commit c6cbdb5

3 files changed

Lines changed: 55 additions & 7 deletions

File tree

Lib/profiling/sampling/heatmap_collector.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -783,14 +783,16 @@ def _generate_file_html(self, output_path: Path, filename: str,
783783
line_counts: Dict[int, int], self_counts: Dict[int, int],
784784
file_stat: FileStats):
785785
"""Generate HTML for a single source file with heatmap coloring."""
786-
# Read source file
786+
_MAX_SOURCE_SIZE = 10 * 1024 * 1024
787+
source_lines = [f"# Source file not available: {filename}"]
787788
try:
788-
source_lines = Path(filename).read_text(encoding='utf-8', errors='replace').splitlines()
789-
except (IOError, OSError) as e:
790-
if not (filename.startswith('<') or filename.startswith('[') or
791-
filename in ('~', '...', '.') or len(filename) < 2):
792-
print(f"Warning: Could not read source file {filename}: {e}")
793-
source_lines = [f"# Source file not available: {filename}"]
789+
resolved = Path(filename).resolve()
790+
if (resolved.is_file()
791+
and resolved.stat().st_size <= _MAX_SOURCE_SIZE):
792+
source_lines = resolved.read_text(
793+
encoding='utf-8', errors='replace').splitlines()
794+
except (IOError, OSError):
795+
pass
794796

795797
# Generate HTML for each line
796798
max_samples = max(line_counts.values()) if line_counts else 1

Lib/test/test_profiling/test_heatmap.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,50 @@ def test_export_file_html_has_line_numbers(self):
622622
# Should have line-related content
623623
self.assertIn('line-', content)
624624

625+
def test_export_skips_nonexistent_source(self):
626+
"""Source files that do not exist produce a placeholder."""
627+
collector = HeatmapCollector(sample_interval_usec=100)
628+
629+
frames = [('/no/such/file.py', (1, 1, -1, -1), 'f', None)]
630+
collector.process_frames(frames, thread_id=1)
631+
632+
output_path = os.path.join(self.test_dir, 'missing_src')
633+
634+
with captured_stdout(), captured_stderr():
635+
collector.export(output_path)
636+
637+
html_files = [f for f in os.listdir(output_path)
638+
if f.startswith('file_') and f.endswith('.html')]
639+
if html_files:
640+
with open(os.path.join(output_path, html_files[0]),
641+
'r', encoding='utf-8') as f:
642+
content = f.read()
643+
self.assertIn('Source file not available', content)
644+
645+
def test_export_caps_oversized_source(self):
646+
"""Source files larger than the cap produce a placeholder."""
647+
collector = HeatmapCollector(sample_interval_usec=100)
648+
649+
big_file = os.path.join(self.test_dir, 'big.py')
650+
with open(big_file, 'w') as f:
651+
f.write('x = 1\n' * 2_000_000)
652+
653+
frames = [(big_file, (1, 1, -1, -1), 'f', None)]
654+
collector.process_frames(frames, thread_id=1)
655+
656+
output_path = os.path.join(self.test_dir, 'big_src')
657+
658+
with captured_stdout(), captured_stderr():
659+
collector.export(output_path)
660+
661+
html_files = [f for f in os.listdir(output_path)
662+
if f.startswith('file_') and f.endswith('.html')]
663+
if html_files:
664+
with open(os.path.join(output_path, html_files[0]),
665+
'r', encoding='utf-8') as f:
666+
content = f.read()
667+
self.assertIn('Source file not available', content)
668+
625669

626670
class MockFrameInfo:
627671
"""Mock FrameInfo for testing.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Skip non-regular and oversized files in the ``profiling.sampling`` heatmap
2+
exporter instead of reading them unconditionally. Patch by tonghuaroot.

0 commit comments

Comments
 (0)