Skip to content

Commit bbf3b99

Browse files
stats: add output_file option to output the stats to a file
1 parent 13bb69f commit bbf3b99

File tree

2 files changed

+51
-8
lines changed

2 files changed

+51
-8
lines changed

src/python/pants/goal/stats_aggregator.py

+35-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88
from collections import Counter
99
from dataclasses import dataclass
10+
from typing import Optional
1011

1112
from pants.engine.internals.scheduler import Workunit
1213
from pants.engine.rules import collect_rules, rule
@@ -17,7 +18,7 @@
1718
WorkunitsCallbackFactoryRequest,
1819
)
1920
from pants.engine.unions import UnionRule
20-
from pants.option.option_types import BoolOption
21+
from pants.option.option_types import BoolOption, StrOption
2122
from pants.option.subsystem import Subsystem
2223
from pants.util.collections import deep_getsizeof
2324
from pants.util.strutil import softwrap
@@ -55,13 +56,30 @@ class StatsAggregatorSubsystem(Subsystem):
5556
),
5657
advanced=True,
5758
)
59+
output_file = StrOption(
60+
default=None,
61+
metavar="<path>",
62+
help="Output the stats to this file. If unspecified, outputs to stdout.",
63+
)
64+
65+
66+
def log_or_write_to_file(output_file: Optional[str], text: str, logger: logging.Logger) -> None:
67+
"""Send text to the stdout or write to the output file."""
68+
if output_file:
69+
with open(output_file, "w") as fh:
70+
fh.write(text)
71+
else:
72+
logger.info(text)
5873

5974

6075
class StatsAggregatorCallback(WorkunitsCallback):
61-
def __init__(self, *, log: bool, memory: bool, has_histogram_module: bool) -> None:
76+
def __init__(
77+
self, *, log: bool, memory: bool, output_file: Optional[str], has_histogram_module: bool
78+
) -> None:
6279
super().__init__()
6380
self.log = log
6481
self.memory = memory
82+
self.output_file = output_file
6583
self.has_histogram_module = has_histogram_module
6684

6785
@property
@@ -80,6 +98,8 @@ def __call__(
8098
if not finished:
8199
return
82100

101+
output_contents = ""
102+
83103
if self.log:
84104
# Capture global counters.
85105
counters = Counter(context.get_metrics())
@@ -93,7 +113,7 @@ def __call__(
93113
counter_lines = "\n".join(
94114
f" {name}: {count}" for name, count in sorted(counters.items())
95115
)
96-
logger.info(f"Counters:\n{counter_lines}")
116+
output_contents += f"Counters:\n{counter_lines}"
97117

98118
if self.memory:
99119
ids: set[int] = set()
@@ -115,18 +135,23 @@ def __call__(
115135
memory_lines = "\n".join(
116136
f" {size}\t\t{count}\t\t{name}" for size, count, name in sorted(entries)
117137
)
118-
logger.info(f"Memory summary (total size in bytes, count, name):\n{memory_lines}")
138+
output_contents += (
139+
f"\nMemory summary (total size in bytes, count, name):\n{memory_lines}"
140+
)
119141

120142
if not (self.log and self.has_histogram_module):
143+
log_or_write_to_file(self.output_file, output_contents, logger)
121144
return
145+
122146
from hdrh.histogram import HdrHistogram # pants: no-infer-dep
123147

124148
histograms = context.get_observation_histograms()["histograms"]
125149
if not histograms:
126-
logger.info("No observation histogram were recorded.")
150+
output_contents += "\nNo observation histogram were recorded."
151+
log_or_write_to_file(self.output_file, output_contents, logger)
127152
return
128153

129-
logger.info("Observation histogram summaries:")
154+
output_contents += "\nObservation histogram summaries:"
130155
for name, encoded_histogram in histograms.items():
131156
# Note: The Python library for HDR Histogram will only decode compressed histograms
132157
# that are further encoded with base64. See
@@ -138,8 +163,8 @@ def __call__(
138163
[25, 50, 75, 90, 95, 99]
139164
).items()
140165
)
141-
logger.info(
142-
f"Summary of `{name}` observation histogram:\n"
166+
output_contents += (
167+
f"\nSummary of `{name}` observation histogram:\n"
143168
f" min: {histogram.get_min_value()}\n"
144169
f" max: {histogram.get_max_value()}\n"
145170
f" mean: {histogram.get_mean_value():.3f}\n"
@@ -148,6 +173,7 @@ def __call__(
148173
f" sum: {int(histogram.get_mean_value() * histogram.total_count)}\n"
149174
f"{percentile_to_vals}"
150175
)
176+
log_or_write_to_file(self.output_file, output_contents, logger)
151177

152178

153179
@dataclass(frozen=True)
@@ -178,6 +204,7 @@ def construct_callback(
178204
StatsAggregatorCallback(
179205
log=subsystem.log,
180206
memory=subsystem.memory_summary,
207+
output_file=subsystem.output_file,
181208
has_histogram_module=has_histogram_module,
182209
)
183210
if subsystem.log or subsystem.memory_summary

src/python/pants/goal/stats_aggregator_integration_test.py

+16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from __future__ import annotations
55

66
import re
7+
from pathlib import Path
78

89
from pants.testutil.pants_integration_test import run_pants, setup_tmpdir
910

@@ -46,3 +47,18 @@ def test_warn_if_no_histograms() -> None:
4647
assert "Counters:" in result.stderr
4748
assert "Please run with `--plugins=hdrhistogram`" in result.stderr
4849
assert "Observation histogram summaries:" not in result.stderr
50+
51+
52+
def test_writing_to_output_file() -> None:
53+
with setup_tmpdir({"src/py/app.py": "print(0)\n", "src/py/BUILD": "python_sources()"}):
54+
argv = [
55+
"--backend-packages=['pants.backend.python']",
56+
"--stats-log",
57+
"--stats-memory-summary",
58+
"--stats-output-file=stats.txt",
59+
"roots",
60+
]
61+
run_pants(argv).assert_success()
62+
output_file_contents = Path("stats.txt").read_text()
63+
for item in ("Counters:", "Memory summary"):
64+
assert item in output_file_contents

0 commit comments

Comments
 (0)