Coverage for python/lsst/verify/tasks/commonMetrics.py: 38%

73 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-03-12 01:54 -0800

1# 

2# This file is part of verify. 

3# 

4# Developed for the LSST Data Management System. 

5# This product includes software developed by the LSST Project 

6# (http://www.lsst.org). 

7# See the COPYRIGHT file at the top-level directory of this distribution 

8# for details of code ownership. 

9# 

10# This program is free software: you can redistribute it and/or modify 

11# it under the terms of the GNU General Public License as published by 

12# the Free Software Foundation, either version 3 of the License, or 

13# (at your option) any later version. 

14# 

15# This program is distributed in the hope that it will be useful, 

16# but WITHOUT ANY WARRANTY; without even the implied warranty of 

17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18# GNU General Public License for more details. 

19# 

20# You should have received a copy of the GNU General Public License 

21# along with this program. If not, see <http://www.gnu.org/licenses/>. 

22# 

23 

24"""Code for measuring metrics that apply to any Task. 

25""" 

26 

27__all__ = ["TimingMetricConfig", "TimingMetricTask", 

28 "MemoryMetricConfig", "MemoryMetricTask", 

29 ] 

30 

31import resource 

32import sys 

33import warnings 

34 

35import astropy.units as u 

36 

37import lsst.pex.config as pexConfig 

38 

39from lsst.verify import Measurement, Datum 

40from lsst.verify.gen2tasks.metricRegistry import registerMultiple 

41from lsst.verify.tasks import MetricComputationError, MetadataMetricTask, \ 

42 MetadataMetricConfig 

43 

44 

45class TimeMethodMetricConfig(MetadataMetricConfig): 

46 """Common config fields for metrics based on `~lsst.pipe.base.timeMethod`. 

47 

48 These fields let metrics distinguish between different methods that have 

49 been decorated with `~lsst.pipe.base.timeMethod`. 

50 """ 

51 target = pexConfig.Field( 

52 dtype=str, 

53 doc="The method to profile, optionally prefixed by one or more tasks " 

54 "in the format of `lsst.pipe.base.Task.getFullMetadata()`.") 

55 metric = pexConfig.Field( 

56 dtype=str, 

57 optional=True, 

58 doc="The fully qualified name of the metric to store the " 

59 "profiling information.", 

60 deprecated="This field has been replaced by connections.package and " 

61 "connections.metric. It will be removed along " 

62 "with daf_persistence." 

63 ) 

64 

65 def validate(self): 

66 super().validate() 

67 

68 if self.metric: 

69 if self.metric != self.connections.package \ 

70 + "." + self.connections.metric: 

71 warnings.warn( 

72 "config.metric is deprecated; set connections.package " 

73 "and connections.metric instead.", 

74 FutureWarning) 

75 try: 

76 self.connections.package, self.connections.metric \ 

77 = self.metric.split(".") 

78 except ValueError: 

79 self.connections.package = "" 

80 self.connections.metric = self.metric 

81 

82 

83# Expose TimingMetricConfig name because config-writers expect it 

84TimingMetricConfig = TimeMethodMetricConfig 

85 

86 

87@registerMultiple("timing") 

88class TimingMetricTask(MetadataMetricTask): 

89 """A Task that computes a wall-clock time using metadata produced by the 

90 `lsst.pipe.base.timeMethod` decorator. 

91 

92 Parameters 

93 ---------- 

94 args 

95 kwargs 

96 Constructor parameters are the same as for 

97 `lsst.verify.tasks.MetricTask`. 

98 """ 

99 

100 ConfigClass = TimingMetricConfig 

101 _DefaultName = "timingMetric" 

102 

103 @classmethod 

104 def getInputMetadataKeys(cls, config): 

105 """Get search strings for the metadata. 

106 

107 Parameters 

108 ---------- 

109 config : ``cls.ConfigClass`` 

110 Configuration for this task. 

111 

112 Returns 

113 ------- 

114 keys : `dict` 

115 A dictionary of keys, optionally prefixed by one or more tasks in 

116 the format of `lsst.pipe.base.Task.getFullMetadata()`. 

117 

118 ``"StartTime"`` 

119 The key for when the target method started (`str`). 

120 ``"EndTime"`` 

121 The key for when the target method ended (`str`). 

122 """ 

123 keyBase = config.target 

124 return {"StartTime": keyBase + "StartCpuTime", 

125 "EndTime": keyBase + "EndCpuTime", 

126 "StartTimestamp": keyBase + "StartUtc", 

127 "EndTimestamp": keyBase + "EndUtc", 

128 } 

129 

130 def makeMeasurement(self, timings): 

131 """Compute a wall-clock measurement from metadata provided by 

132 `lsst.pipe.base.timeMethod`. 

133 

134 Parameters 

135 ---------- 

136 timings : `dict` [`str`, any] 

137 A representation of the metadata passed to `run`. The `dict` has 

138 the following keys: 

139 

140 ``"StartTime"`` 

141 The time the target method started (`float` or `None`). 

142 ``"EndTime"`` 

143 The time the target method ended (`float` or `None`). 

144 ``"StartTimestamp"``, ``"EndTimestamp"`` 

145 The start and end timestamps, in an ISO 8601-compliant format 

146 (`str` or `None`). 

147 

148 Returns 

149 ------- 

150 measurement : `lsst.verify.Measurement` or `None` 

151 The running time of the target method. 

152 

153 Raises 

154 ------ 

155 MetricComputationError 

156 Raised if the timing metadata are invalid. 

157 """ 

158 if timings["StartTime"] is not None or timings["EndTime"] is not None: 

159 try: 

160 totalTime = timings["EndTime"] - timings["StartTime"] 

161 except TypeError: 

162 raise MetricComputationError("Invalid metadata") 

163 else: 

164 meas = Measurement(self.config.metricName, 

165 totalTime * u.second) 

166 meas.notes["estimator"] = "pipe.base.timeMethod" 

167 if timings["StartTimestamp"]: 

168 meas.extras["start"] = Datum(timings["StartTimestamp"]) 

169 if timings["EndTimestamp"]: 

170 meas.extras["end"] = Datum(timings["EndTimestamp"]) 

171 return meas 

172 else: 

173 self.log.info("Nothing to do: no timing information for %s found.", 

174 self.config.target) 

175 return None 

176 

177 

178# Expose MemoryMetricConfig name because config-writers expect it 

179MemoryMetricConfig = TimeMethodMetricConfig 

180 

181 

182@registerMultiple("memory") 

183class MemoryMetricTask(MetadataMetricTask): 

184 """A Task that computes the maximum resident set size using metadata 

185 produced by the `lsst.pipe.base.timeMethod` decorator. 

186 

187 Parameters 

188 ---------- 

189 args 

190 kwargs 

191 Constructor parameters are the same as for 

192 `lsst.verify.tasks.MetricTask`. 

193 """ 

194 

195 ConfigClass = MemoryMetricConfig 

196 _DefaultName = "memoryMetric" 

197 

198 @classmethod 

199 def getInputMetadataKeys(cls, config): 

200 """Get search strings for the metadata. 

201 

202 Parameters 

203 ---------- 

204 config : ``cls.ConfigClass`` 

205 Configuration for this task. 

206 

207 Returns 

208 ------- 

209 keys : `dict` 

210 A dictionary of keys, optionally prefixed by one or more tasks in 

211 the format of `lsst.pipe.base.Task.getFullMetadata()`. 

212 

213 ``"EndMemory"`` 

214 The key for the memory usage at the end of the method (`str`). 

215 """ 

216 keyBase = config.target 

217 return {"EndMemory": keyBase + "EndMaxResidentSetSize"} 

218 

219 def makeMeasurement(self, memory): 

220 """Compute a maximum resident set size measurement from metadata 

221 provided by `lsst.pipe.base.timeMethod`. 

222 

223 Parameters 

224 ---------- 

225 memory : `dict` [`str`, any] 

226 A representation of the metadata passed to `run`. Each `dict` has 

227 the following keys: 

228 

229 ``"EndMemory"`` 

230 The memory usage at the end of the method (`int` or `None`). 

231 

232 Returns 

233 ------- 

234 measurement : `lsst.verify.Measurement` or `None` 

235 The maximum memory usage of the target method. 

236 

237 Raises 

238 ------ 

239 MetricComputationError 

240 Raised if the memory metadata are invalid. 

241 """ 

242 if memory["EndMemory"] is not None: 

243 try: 

244 maxMemory = int(memory["EndMemory"]) 

245 except (ValueError, TypeError) as e: 

246 raise MetricComputationError("Invalid metadata") from e 

247 else: 

248 meas = Measurement(self.config.metricName, 

249 self._addUnits(maxMemory)) 

250 meas.notes['estimator'] = 'pipe.base.timeMethod' 

251 return meas 

252 else: 

253 self.log.info("Nothing to do: no memory information for %s found.", 

254 self.config.target) 

255 return None 

256 

257 def _addUnits(self, memory): 

258 """Represent memory usage in correct units. 

259 

260 Parameters 

261 ---------- 

262 memory : `int` 

263 The memory usage as returned by `resource.getrusage`, in 

264 platform-dependent units. 

265 

266 Returns 

267 ------- 

268 memory : `astropy.units.Quantity` 

269 The memory usage in absolute units. 

270 """ 

271 if sys.platform.startswith('darwin'): 

272 # MacOS uses bytes 

273 return memory * u.byte 

274 elif sys.platform.startswith('sunos') \ 

275 or sys.platform.startswith('solaris'): 

276 # Solaris and SunOS use pages 

277 return memory * resource.getpagesize() * u.byte 

278 else: 

279 # Assume Linux, which uses kibibytes 

280 return memory * u.kibibyte