7
7
import functools
8
8
import inspect
9
9
import linecache
10
+ import operator
10
11
import os
11
12
import pickle
12
13
import sys
17
18
from datetime import datetime
18
19
19
20
try :
20
- from ._line_profiler import LineProfiler as CLineProfiler
21
+ from ._line_profiler import (LineProfiler as CLineProfiler ,
22
+ LineStats as CLineStats )
21
23
except ImportError as ex :
22
24
raise ImportError (
23
25
'The line_profiler._line_profiler c-extension is not importable. '
@@ -186,6 +188,169 @@ def __init__(self, func, profiler_id):
186
188
self .profiler_id = profiler_id
187
189
188
190
191
+ class LineStats (CLineStats ):
192
+ def __repr__ (self ):
193
+ return '{}({}, {:.2G})' .format (
194
+ type (self ).__name__ , self .timings , self .unit )
195
+
196
+ def __eq__ (self , other ):
197
+ """
198
+ Example:
199
+ >>> from copy import deepcopy
200
+ >>>
201
+ >>>
202
+ >>> stats1 = LineStats(
203
+ ... {('foo', 1, 'spam.py'): [(2, 10, 300)],
204
+ ... ('bar', 10, 'spam.py'):
205
+ ... [(11, 2, 1000), (12, 1, 500)]},
206
+ ... 1E-6)
207
+ >>> stats2 = deepcopy(stats1)
208
+ >>> assert stats1 == stats2 is not stats1
209
+ >>> stats2.timings = 1E-7
210
+ >>> assert stats2 != stats1
211
+ >>> stats3 = deepcopy(stats1)
212
+ >>> assert stats1 == stats3 is not stats1
213
+ >>> stats3.timings['foo', 1, 'spam.py'][:] = [(2, 11, 330)]
214
+ >>> assert stats3 != stats1
215
+ """
216
+ for attr in 'timings' , 'unit' :
217
+ getter = operator .attrgetter (attr )
218
+ try :
219
+ if getter (self ) != getter (other ):
220
+ return False
221
+ except (AttributeError , TypeError ):
222
+ return NotImplemented
223
+ return True
224
+
225
+ def __add__ (self , other ):
226
+ """
227
+ Example:
228
+ >>> stats1 = LineStats(
229
+ ... {('foo', 1, 'spam.py'): [(2, 10, 300)],
230
+ ... ('bar', 10, 'spam.py'):
231
+ ... [(11, 2, 1000), (12, 1, 500)]},
232
+ ... 1E-6)
233
+ >>> stats2 = LineStats(
234
+ ... {('bar', 10, 'spam.py'):
235
+ ... [(11, 10, 20000), (12, 5, 1000)],
236
+ ... ('baz', 5, 'eggs.py'): [(5, 2, 5000)]},
237
+ ... 1E-7)
238
+ >>> stats_sum = LineStats(
239
+ ... {('foo', 1, 'spam.py'): [(2, 10, 300)],
240
+ ... ('bar', 10, 'spam.py'):
241
+ ... [(11, 12, 3000), (12, 6, 600)],
242
+ ... ('baz', 5, 'eggs.py'): [(5, 2, 500)]},
243
+ ... 1E-6)
244
+ >>> assert stats1 + stats2 == stats2 + stats1 == stats_sum
245
+ """
246
+ timings , unit = self ._get_aggregated_timings ([self , other ])
247
+ return type (self )(timings , unit )
248
+
249
+ def __iadd__ (self , other ):
250
+ """
251
+ Example:
252
+ >>> stats1 = LineStats(
253
+ ... {('foo', 1, 'spam.py'): [(2, 10, 300)],
254
+ ... ('bar', 10, 'spam.py'):
255
+ ... [(11, 2, 1000), (12, 1, 500)]},
256
+ ... 1E-6)
257
+ >>> stats2 = LineStats(
258
+ ... {('bar', 10, 'spam.py'):
259
+ ... [(11, 10, 20000), (12, 5, 1000)],
260
+ ... ('baz', 5, 'eggs.py'): [(5, 2, 5000)]},
261
+ ... 1E-7)
262
+ >>> stats_sum = LineStats(
263
+ ... {('foo', 1, 'spam.py'): [(2, 10, 300)],
264
+ ... ('bar', 10, 'spam.py'):
265
+ ... [(11, 12, 3000), (12, 6, 600)],
266
+ ... ('baz', 5, 'eggs.py'): [(5, 2, 500)]},
267
+ ... 1E-6)
268
+ >>> address = id(stats2)
269
+ >>> stats2 += stats1
270
+ >>> assert id(stats2) == address
271
+ >>> assert stats2 == stats_sum
272
+ """
273
+ self .timings , self .unit = self ._get_aggregated_timings ([self , other ])
274
+ return self
275
+
276
+ def print (self , stream = None , ** kwargs ):
277
+ show_text (self .timings , self .unit , stream = stream , ** kwargs )
278
+
279
+ def to_file (self , filename ):
280
+ """ Pickle the instance to the given filename.
281
+ """
282
+ with open (filename , 'wb' ) as f :
283
+ pickle .dump (self , f , pickle .HIGHEST_PROTOCOL )
284
+
285
+ @classmethod
286
+ def from_files (cls , file , / , * files ):
287
+ """
288
+ Utility function to load an instance from the given filenames.
289
+ """
290
+ stats_objs = []
291
+ for file in [file , * files ]:
292
+ with open (file , 'rb' ) as f :
293
+ stats_objs .append (pickle .load (f ))
294
+ return cls .from_stats_objects (* stats_objs )
295
+
296
+ @classmethod
297
+ def from_stats_objects (cls , stats , / , * more_stats ):
298
+ """
299
+ Example:
300
+ >>> stats1 = LineStats(
301
+ ... {('foo', 1, 'spam.py'): [(2, 10, 300)],
302
+ ... ('bar', 10, 'spam.py'):
303
+ ... [(11, 2, 1000), (12, 1, 500)]},
304
+ ... 1E-6)
305
+ >>> stats2 = LineStats(
306
+ ... {('bar', 10, 'spam.py'):
307
+ ... [(11, 10, 20000), (12, 5, 1000)],
308
+ ... ('baz', 5, 'eggs.py'): [(5, 2, 5000)]},
309
+ ... 1E-7)
310
+ >>> stats_combined = LineStats.from_stats_objects(
311
+ ... stats1, stats2)
312
+ >>> assert stats_combined.unit == 1E-6
313
+ >>> assert stats_combined.timings == {
314
+ ... ('foo', 1, 'spam.py'): [(2, 10, 300)],
315
+ ... ('bar', 10, 'spam.py'):
316
+ ... [(11, 12, 3000), (12, 6, 600)],
317
+ ... ('baz', 5, 'eggs.py'): [(5, 2, 500)]}
318
+ """
319
+ timings , unit = cls ._get_aggregated_timings ([stats , * more_stats ])
320
+ return cls (timings , unit )
321
+
322
+ @staticmethod
323
+ def _get_aggregated_timings (stats_objs ):
324
+ if not stats_objs :
325
+ raise ValueError (f'stats_objs = { stats_objs !r} : empty' )
326
+ try :
327
+ stats , = stats_objs
328
+ except ValueError : # > 1 obj
329
+ # Add from small scaling factors to large to minimize
330
+ # rounding errors
331
+ stats_objs = sorted (stats_objs , key = operator .attrgetter ('unit' ))
332
+ unit = stats_objs [- 1 ].unit
333
+ # type: dict[tuple[str, int, int], dict[int, tuple[int, float]]
334
+ timing_dict = {}
335
+ for stats in stats_objs :
336
+ factor = stats .unit / unit
337
+ for key , entries in stats .timings .items ():
338
+ entry_dict = timing_dict .setdefault (key , {})
339
+ for lineno , nhits , time in entries :
340
+ prev_nhits , prev_time = entry_dict .get (lineno , (0 , 0 ))
341
+ entry_dict [lineno ] = (prev_nhits + nhits ,
342
+ prev_time + factor * time )
343
+ timings = {
344
+ key : [(lineno , nhits , int (round (time , 0 )))
345
+ for lineno , (nhits , time ) in sorted (entry_dict .items ())]
346
+ for key , entry_dict in timing_dict .items ()}
347
+ else :
348
+ timings = {key : entries .copy ()
349
+ for key , entries in stats .timings .items ()}
350
+ unit = stats .unit
351
+ return timings , unit
352
+
353
+
189
354
class LineProfiler (CLineProfiler , ByCountProfilerMixin ):
190
355
"""
191
356
A profiler that records the execution times of individual lines.
@@ -296,24 +461,24 @@ def _debug(self, msg):
296
461
msg = f'{ self_repr } : { msg } '
297
462
logger .debug (msg )
298
463
464
+ def get_stats (self ):
465
+ return LineStats .from_stats_objects (super ().get_stats ())
466
+
299
467
def dump_stats (self , filename ):
300
468
""" Dump a representation of the data to a file as a pickled
301
469
:py:class:`~.LineStats` object from :py:meth:`~.get_stats()`.
302
470
"""
303
- lstats = self .get_stats ()
304
- with open (filename , 'wb' ) as f :
305
- pickle .dump (lstats , f , pickle .HIGHEST_PROTOCOL )
471
+ self .get_stats ().to_file (filename )
306
472
307
473
def print_stats (self , stream = None , output_unit = None , stripzeros = False ,
308
474
details = True , summarize = False , sort = False , rich = False , * ,
309
475
config = None ):
310
476
""" Show the gathered statistics.
311
477
"""
312
- lstats = self .get_stats ()
313
- show_text (lstats .timings , lstats .unit , output_unit = output_unit ,
314
- stream = stream , stripzeros = stripzeros ,
315
- details = details , summarize = summarize , sort = sort , rich = rich ,
316
- config = config )
478
+ self .get_stats ().print (
479
+ stream = stream , output_unit = output_unit ,
480
+ stripzeros = stripzeros , details = details , summarize = summarize ,
481
+ sort = sort , rich = rich , config = config )
317
482
318
483
def _add_namespace (
319
484
self , namespace , * ,
@@ -799,12 +964,7 @@ def show_text(stats, unit, output_unit=None, stream=None, stripzeros=False,
799
964
stream .write (line + '\n ' )
800
965
801
966
802
- def load_stats (filename ):
803
- """ Utility function to load a pickled :py:class:`~.LineStats`
804
- object from a given filename.
805
- """
806
- with open (filename , 'rb' ) as f :
807
- return pickle .load (f )
967
+ load_stats = LineStats .from_files
808
968
809
969
810
970
def main ():
@@ -846,7 +1006,8 @@ def main():
846
1006
help = 'Print a summary of total function time. '
847
1007
f'(Default: { default .conf_dict ["summarize" ]} )' )
848
1008
add_argument (parser , 'profile_output' ,
849
- help = "'*.lprof' file created by `kernprof`" )
1009
+ nargs = '+' ,
1010
+ help = "'*.lprof' file(s) created by `kernprof`" )
850
1011
851
1012
args = parser .parse_args ()
852
1013
if args .config :
@@ -856,7 +1017,7 @@ def main():
856
1017
if getattr (args , key , None ) is None :
857
1018
setattr (args , key , default )
858
1019
859
- lstats = load_stats ( args .profile_output )
1020
+ lstats = LineStats . from_files ( * args .profile_output )
860
1021
show_text (lstats .timings , lstats .unit ,
861
1022
output_unit = args .unit ,
862
1023
stripzeros = args .skip_zero ,
0 commit comments