4
4
from __future__ import annotations
5
5
6
6
import base64
7
+ import datetime
7
8
import logging
8
9
from collections import Counter
9
10
from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Optional
10
13
11
14
from pants .engine .internals .scheduler import Workunit
12
15
from pants .engine .rules import collect_rules , rule
17
20
WorkunitsCallbackFactoryRequest ,
18
21
)
19
22
from pants .engine .unions import UnionRule
20
- from pants .option .option_types import BoolOption
23
+ from pants .option .option_types import BoolOption , StrOption
21
24
from pants .option .subsystem import Subsystem
22
25
from pants .util .collections import deep_getsizeof
26
+ from pants .util .dirutil import safe_open
23
27
from pants .util .strutil import softwrap
24
28
25
29
logger = logging .getLogger (__name__ )
@@ -55,13 +59,33 @@ class StatsAggregatorSubsystem(Subsystem):
55
59
),
56
60
advanced = True ,
57
61
)
62
+ output_file = StrOption (
63
+ default = None ,
64
+ metavar = "<path>" ,
65
+ help = "Output the stats to this file. If unspecified, outputs to stdout." ,
66
+ )
67
+
68
+
69
+ def _log_or_write_to_file (output_file : Optional [str ], lines : list [str ]) -> None :
70
+ """Send text to the stdout or write to the output file."""
71
+ if lines :
72
+ text = "\n " .join (lines )
73
+ if output_file :
74
+ with safe_open (output_file , "a" ) as fh :
75
+ fh .write (text )
76
+ logger .info (f"Wrote Pants stats to { output_file } " )
77
+ else :
78
+ logger .info (text )
58
79
59
80
60
81
class StatsAggregatorCallback (WorkunitsCallback ):
61
- def __init__ (self , * , log : bool , memory : bool , has_histogram_module : bool ) -> None :
82
+ def __init__ (
83
+ self , * , log : bool , memory : bool , output_file : Optional [str ], has_histogram_module : bool
84
+ ) -> None :
62
85
super ().__init__ ()
63
86
self .log = log
64
87
self .memory = memory
88
+ self .output_file = output_file
65
89
self .has_histogram_module = has_histogram_module
66
90
67
91
@property
@@ -80,6 +104,15 @@ def __call__(
80
104
if not finished :
81
105
return
82
106
107
+ output_lines = []
108
+ if self .output_file :
109
+ timestamp = datetime .datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" )
110
+ # have an empty line between stats of different Pants invocations
111
+ space = "\n \n " if Path (self .output_file ).exists () else ""
112
+ output_lines .append (
113
+ f"{ space } { timestamp } Command: { context .run_tracker .run_information ().get ('cmd_line' )} "
114
+ )
115
+
83
116
if self .log :
84
117
# Capture global counters.
85
118
counters = Counter (context .get_metrics ())
@@ -93,7 +126,7 @@ def __call__(
93
126
counter_lines = "\n " .join (
94
127
f" { name } : { count } " for name , count in sorted (counters .items ())
95
128
)
96
- logger . info (f"Counters:\n { counter_lines } " )
129
+ output_lines . append (f"Counters:\n { counter_lines } " )
97
130
98
131
if self .memory :
99
132
ids : set [int ] = set ()
@@ -115,18 +148,23 @@ def __call__(
115
148
memory_lines = "\n " .join (
116
149
f" { size } \t \t { count } \t \t { name } " for size , count , name in sorted (entries )
117
150
)
118
- logger .info (f"Memory summary (total size in bytes, count, name):\n { memory_lines } " )
151
+ output_lines .append (
152
+ f"Memory summary (total size in bytes, count, name):\n { memory_lines } "
153
+ )
119
154
120
155
if not (self .log and self .has_histogram_module ):
156
+ _log_or_write_to_file (self .output_file , output_lines )
121
157
return
158
+
122
159
from hdrh .histogram import HdrHistogram # pants: no-infer-dep
123
160
124
161
histograms = context .get_observation_histograms ()["histograms" ]
125
162
if not histograms :
126
- logger .info ("No observation histogram were recorded." )
163
+ output_lines .append ("No observation histogram were recorded." )
164
+ _log_or_write_to_file (self .output_file , output_lines )
127
165
return
128
166
129
- logger . info ("Observation histogram summaries:" )
167
+ output_lines . append ("Observation histogram summaries:" )
130
168
for name , encoded_histogram in histograms .items ():
131
169
# Note: The Python library for HDR Histogram will only decode compressed histograms
132
170
# that are further encoded with base64. See
@@ -138,7 +176,7 @@ def __call__(
138
176
[25 , 50 , 75 , 90 , 95 , 99 ]
139
177
).items ()
140
178
)
141
- logger . info (
179
+ output_lines . append (
142
180
f"Summary of `{ name } ` observation histogram:\n "
143
181
f" min: { histogram .get_min_value ()} \n "
144
182
f" max: { histogram .get_max_value ()} \n "
@@ -148,6 +186,7 @@ def __call__(
148
186
f" sum: { int (histogram .get_mean_value () * histogram .total_count )} \n "
149
187
f"{ percentile_to_vals } "
150
188
)
189
+ _log_or_write_to_file (self .output_file , output_lines )
151
190
152
191
153
192
@dataclass (frozen = True )
@@ -178,6 +217,7 @@ def construct_callback(
178
217
StatsAggregatorCallback (
179
218
log = subsystem .log ,
180
219
memory = subsystem .memory_summary ,
220
+ output_file = subsystem .output_file ,
181
221
has_histogram_module = has_histogram_module ,
182
222
)
183
223
if subsystem .log or subsystem .memory_summary
0 commit comments