filename: Ignored (binary files are written incrementally)
"""
self._writer.finalize()
+ return True
@property
def total_samples(self):
"binary": BinaryCollector,
}
+BROWSER_COMPATIBLE_FORMATS = ("flamegraph", "diff_flamegraph", "heatmap")
+
+
def _setup_child_monitor(args, parent_pid):
# Build CLI args for child profilers (excluding --subprocesses to avoid recursion)
child_cli_args = _build_child_profiler_args(args)
output_group.add_argument(
"--browser",
action="store_true",
- help="Automatically open HTML output (flamegraph, heatmap) in browser. "
- "When using `--subprocesses`, only the main process opens the browser",
+ help=(
+ "Automatically open HTML output "
+ f"({', '.join('--' + f.replace('_', '-') for f in BROWSER_COMPATIBLE_FORMATS)}) "
+ "in browser. "
+ "When using `--subprocesses`, only the main process opens the browser"
+ ),
)
args.outfile
or _generate_output_filename(args.format, os.getpid())
)
- collector.export(filename)
+ export_ok = collector.export(filename)
# Auto-open browser for HTML output if --browser flag is set
if (
- args.format in (
- 'flamegraph', 'diff_flamegraph', 'heatmap'
- )
+ export_ok
+ and args.format in BROWSER_COMPATIBLE_FORMATS
and getattr(args, 'browser', False)
):
_open_in_browser(filename)
filename = os.path.join(args.outfile, _generate_output_filename(args.format, pid))
else:
filename = args.outfile or _generate_output_filename(args.format, pid)
- collector.export(filename)
+ export_ok = collector.export(filename)
# Auto-open browser for HTML output if --browser flag is set
- if args.format in ('flamegraph', 'diff_flamegraph', 'heatmap') and getattr(args, 'browser', False):
+ if (
+ export_ok
+ and args.format in BROWSER_COMPATIBLE_FORMATS
+ and getattr(args, 'browser', False)
+ ):
_open_in_browser(filename)
@abstractmethod
def export(self, filename):
- """Export collected data to a file."""
+ """Export collected data.
+
+ Returns:
+ bool: True if output was generated, False if there was no data to export.
+ """
@staticmethod
def _filter_internal_frames(frames):
print(
f"Open in Firefox Profiler: https://profiler.firefox.com/"
)
+ return True
def _build_marker_schema(self):
"""Build marker schema definitions for Firefox Profiler."""
"""
if not self.file_samples:
print("Warning: No heatmap data to export")
- return
+ return False
try:
output_dir = self._prepare_output_directory(output_path)
self._generate_index_html(output_dir / 'index.html', file_stats)
self._print_export_summary(output_dir, file_stats)
+ return True
except Exception as e:
print(f"Error: Failed to export heatmap: {e}")
)
self._write_message(output, self._build_end_record())
print(f"JSONL profile written to {filename}")
+ return True
def _build_meta_record(self):
record = {
def export(self, filename):
self.create_stats()
self._dump_stats(filename)
+ return True
def _dump_stats(self, file):
stats_with_marker = dict(self.stats)
for stack, count in lines:
f.write(f"{stack} {count}\n")
print(f"Collapsed stack output written to {filename}")
+ return True
class FlamegraphCollector(StackTraceCollector):
print(
"Warning: No functions found in profiling data. Check if sampling captured any data."
)
- return
+ return False
html_content = self._create_flamegraph_html(flamegraph_data)
f.write(html_content)
print(f"Flamegraph saved to: {filename}")
+ return True
@staticmethod
@functools.lru_cache(maxsize=None)
import sys
import tempfile
import unittest
+from types import SimpleNamespace
from unittest import mock
try:
FORMAT_EXTENSIONS,
_create_collector,
_generate_output_filename,
+ _handle_output,
main,
)
from profiling.sampling.constants import (
call_kwargs = mock_sample.call_args[1]
self.assertEqual(call_kwargs.get("async_aware"), "running")
+ def test_handle_output_browser_not_opened_when_export_fails(self):
+ for format_type in ("flamegraph", "diff_flamegraph", "heatmap"):
+ with self.subTest(format=format_type):
+ collector = mock.MagicMock()
+ collector.export.return_value = False
+ args = SimpleNamespace(
+ format=format_type,
+ outfile="profile.html",
+ browser=True,
+ )
+
+ with (
+ mock.patch("profiling.sampling.cli.os.path.isdir", return_value=False),
+ mock.patch("profiling.sampling.cli._open_in_browser") as mock_open,
+ ):
+ _handle_output(collector, args, pid=12345, mode=0)
+
+ collector.export.assert_called_once_with("profile.html")
+ mock_open.assert_not_called()
+
def test_async_aware_with_async_mode_all(self):
"""Test --async-aware with --async-mode all."""
test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "--async-mode", "all"]
# Export flamegraph
with captured_stdout(), captured_stderr():
- collector.export(flamegraph_out.name)
+ export_ok = collector.export(flamegraph_out.name)
# Verify file was created and contains valid data
+ self.assertTrue(export_ok)
self.assertTrue(os.path.exists(flamegraph_out.name))
self.assertGreater(os.path.getsize(flamegraph_out.name), 0)
self.assertIn('"value":', content)
self.assertIn('"children":', content)
+ def test_flamegraph_collector_empty_export_fails(self):
+ """Test empty flamegraph export reports no output."""
+ flamegraph_out = tempfile.NamedTemporaryFile(
+ suffix=".html", delete=False
+ )
+ self.addCleanup(close_and_unlink, flamegraph_out)
+
+ collector = FlamegraphCollector(1000)
+
+ with captured_stdout(), captured_stderr():
+ export_ok = collector.export(flamegraph_out.name)
+
+ self.assertFalse(export_ok)
+ self.assertEqual(os.path.getsize(flamegraph_out.name), 0)
+
def test_gecko_collector_basic(self):
"""Test basic GeckoCollector functionality."""
collector = GeckoCollector(1000)
self.addCleanup(close_and_unlink, flamegraph_out)
with captured_stdout(), captured_stderr():
- diff.export(flamegraph_out.name)
+ export_ok = diff.export(flamegraph_out.name)
+ self.assertTrue(export_ok)
self.assertTrue(os.path.exists(flamegraph_out.name))
self.assertGreater(os.path.getsize(flamegraph_out.name), 0)