@@ -51,6 +51,14 @@ def __repr__(self):
51
51
return 's({0.i!r}, {0.num_iters!r}, {0.runtime!r})' .format (self )
52
52
53
53
54
+ class Yield (namedtuple ('Yield' , 'before_sample after' )):
55
+ u"""Meta-measurement of when the Benchmark_X voluntarily yielded process.
56
+
57
+ `before_sample`: index of measurement taken just after returning from yield
58
+ `after`: time elapsed since the previous yield in microseconds (μs)
59
+ """
60
+
61
+
54
62
class PerformanceTestSamples (object ):
55
63
"""Collection of runtime samples from the benchmark execution.
56
64
@@ -69,7 +77,7 @@ def __init__(self, name, samples=None):
69
77
self .add (sample )
70
78
71
79
def __str__ (self ):
72
- """Text summary of benchmark statisctics ."""
80
+ """Text summary of benchmark statistics ."""
73
81
return (
74
82
'{0.name!s} n={0.count!r} '
75
83
'Min={0.min!r} Q1={0.q1!r} M={0.median!r} Q3={0.q3!r} '
@@ -211,31 +219,60 @@ class PerformanceTestResult(object):
211
219
Reported by the test driver (Benchmark_O, Benchmark_Onone, Benchmark_Osize
212
220
or Benchmark_Driver).
213
221
214
- It depends on the log format emitted by the test driver in the form:
215
- #,TEST,SAMPLES,MIN(μs),MAX(μs),MEAN(μs),SD(μs),MEDIAN(μs),MAX_RSS(B)
216
-
217
- The last column, MAX_RSS, is emitted only for runs instrumented by the
218
- Benchmark_Driver to measure rough memory use during the execution of the
219
- benchmark.
222
+ It suppors 2 log formats emitted by the test driver. Legacy format with
223
+ statistics for normal distribution (MEAN, SD):
224
+ #,TEST,SAMPLES,MIN(μs),MAX(μs),MEAN(μs),SD(μs),MEDIAN(μs),MAX_RSS(B)
225
+ And new quantiles format with variable number of columns:
226
+ #,TEST,SAMPLES,MIN(μs),MEDIAN(μs),MAX(μs)
227
+ #,TEST,SAMPLES,MIN(μs),Q1(μs),Q2(μs),Q3(μs),MAX(μs),MAX_RSS(B)
228
+ The number of columns between MIN and MAX depends on the test driver's
229
+ `--quantile`parameter. In both cases, the last column, MAX_RSS is optional.
220
230
"""
221
231
222
- def __init__ (self , csv_row ):
223
- """Initialize from a row with 8 or 9 columns with benchmark summary.
232
+ def __init__ (self , csv_row , quantiles = False , memory = False , delta = False ):
233
+ """Initialize from a row of multiple columns with benchmark summary.
224
234
225
235
The row is an iterable, such as a row provided by the CSV parser.
226
236
"""
227
- self .test_num = csv_row [0 ] # Ordinal number of the test
228
- self .name = csv_row [1 ] # Name of the performance test
229
- self .num_samples = ( # Number of measurement samples taken
230
- int (csv_row [2 ]))
231
- self .min = int (csv_row [3 ]) # Minimum runtime (μs)
232
- self .max = int (csv_row [4 ]) # Maximum runtime (μs)
233
- self .mean = float (csv_row [5 ]) # Mean (average) runtime (μs)
234
- self .sd = float (csv_row [6 ]) # Standard Deviation (μs)
235
- self .median = int (csv_row [7 ]) # Median runtime (μs)
236
- self .max_rss = ( # Maximum Resident Set Size (B)
237
- int (csv_row [8 ]) if len (csv_row ) > 8 else None )
238
- self .samples = None
237
+ self .test_num = csv_row [0 ] # Ordinal number of the test
238
+ self .name = csv_row [1 ] # Name of the performance test
239
+ self .num_samples = int (csv_row [2 ]) # Number of measurements taken
240
+
241
+ if quantiles : # Variable number of columns representing quantiles
242
+ runtimes = csv_row [3 :- 1 ] if memory else csv_row [3 :]
243
+ if delta :
244
+ runtimes = [int (x ) if x else 0 for x in runtimes ]
245
+ runtimes = reduce (lambda l , x : l .append (l [- 1 ] + x ) or # runnin
246
+ l if l else [x ], runtimes , None ) # total
247
+ num_values = len (runtimes )
248
+ if self .num_samples < num_values : # remove repeated samples
249
+ quantile = num_values - 1
250
+ qs = [float (i ) / float (quantile ) for i in range (0 , num_values )]
251
+ indices = [max (0 , int (ceil (self .num_samples * float (q ))) - 1 )
252
+ for q in qs ]
253
+ runtimes = [runtimes [indices .index (i )]
254
+ for i in range (0 , self .num_samples )]
255
+
256
+ self .samples = PerformanceTestSamples (
257
+ self .name ,
258
+ [Sample (None , None , int (runtime )) for runtime in runtimes ])
259
+ self .samples .exclude_outliers (top_only = True )
260
+ sams = self .samples
261
+ self .min , self .max , self .median , self .mean , self .sd = \
262
+ sams .min , sams .max , sams .median , sams .mean , sams .sd
263
+ self .max_rss = ( # Maximum Resident Set Size (B)
264
+ int (csv_row [- 1 ]) if memory else None )
265
+ else : # Legacy format with statistics for normal distribution.
266
+ self .min = int (csv_row [3 ]) # Minimum runtime (μs)
267
+ self .max = int (csv_row [4 ]) # Maximum runtime (μs)
268
+ self .mean = float (csv_row [5 ]) # Mean (average) runtime (μs)
269
+ self .sd = float (csv_row [6 ]) # Standard Deviation (μs)
270
+ self .median = int (csv_row [7 ]) # Median runtime (μs)
271
+ self .max_rss = ( # Maximum Resident Set Size (B)
272
+ int (csv_row [8 ]) if len (csv_row ) > 8 else None )
273
+ self .samples = None
274
+ self .yields = None
275
+ self .setup = None
239
276
240
277
def __repr__ (self ):
241
278
"""Short summary for debugging purposes."""
@@ -253,6 +290,7 @@ def merge(self, r):
253
290
The use case here is comparing test results parsed from concatenated
254
291
log files from multiple runs of benchmark driver.
255
292
"""
293
+ # Statistics
256
294
if self .samples and r .samples :
257
295
map (self .samples .add , r .samples .samples )
258
296
sams = self .samples
@@ -266,8 +304,14 @@ def merge(self, r):
266
304
(self .mean * self .num_samples ) + (r .mean * r .num_samples )
267
305
) / float (self .num_samples + r .num_samples )
268
306
self .num_samples += r .num_samples
269
- self .max_rss = min (self .max_rss , r .max_rss )
270
- self .median , self .sd = 0 , 0
307
+ self .median , self .sd = None , None
308
+
309
+ # Metadata
310
+ def minimum (a , b ): # work around None being less than everything
311
+ return (min (filter (lambda x : x is not None , [a , b ])) if any ([a , b ])
312
+ else None )
313
+ self .max_rss = minimum (self .max_rss , r .max_rss )
314
+ self .setup = minimum (self .setup , r .setup )
271
315
272
316
273
317
class ResultComparison (object ):
@@ -307,40 +351,48 @@ class LogParser(object):
307
351
def __init__ (self ):
308
352
"""Create instance of `LogParser`."""
309
353
self .results = []
354
+ self .quantiles , self .delta , self .memory = False , False , False
310
355
self ._reset ()
311
356
312
357
def _reset (self ):
313
358
"""Reset parser to the default state for reading a new result."""
314
- self .samples , self .num_iters = [], 1
315
- self .max_rss , self .mem_pages = None , None
359
+ self .samples , self .yields , self . num_iters = [], [], 1
360
+ self .setup , self . max_rss , self .mem_pages = None , None , None
316
361
self .voluntary_cs , self .involuntary_cs = None , None
317
362
318
363
# Parse lines like this
319
364
# #,TEST,SAMPLES,MIN(μs),MAX(μs),MEAN(μs),SD(μs),MEDIAN(μs)
320
- results_re = re .compile (r'( *\d+[, \t]*[\w.]+[, \t]*' +
321
- r'[, \t]*' .join ([r'[\d.]+' ] * 6 ) +
322
- r'[, \t]*[\d.]*)' ) # optional MAX_RSS(B)
365
+ results_re = re .compile (
366
+ r'( *\d+[, \t]+[\w.]+[, \t]+' + # #,TEST
367
+ r'[, \t]+' .join ([r'\d+' ] * 2 ) + # at least 2...
368
+ r'(?:[, \t]+\d*)*)' ) # ...or more numeric columns
323
369
324
370
def _append_result (self , result ):
325
- columns = result .split (',' )
326
- if len ( columns ) < 8 :
327
- columns = result . split ()
328
- r = PerformanceTestResult ( columns )
329
- if self .max_rss :
330
- r .max_rss = self .max_rss
331
- r .mem_pages = self .mem_pages
332
- r .voluntary_cs = self .voluntary_cs
333
- r .involuntary_cs = self .involuntary_cs
371
+ columns = result .split (',' ) if ',' in result else result . split ()
372
+ r = PerformanceTestResult (
373
+ columns , quantiles = self . quantiles , memory = self . memory ,
374
+ delta = self . delta )
375
+ r . setup = self .setup
376
+ r . max_rss = r .max_rss or self .max_rss
377
+ r .mem_pages = self .mem_pages
378
+ r .voluntary_cs = self .voluntary_cs
379
+ r .involuntary_cs = self .involuntary_cs
334
380
if self .samples :
335
381
r .samples = PerformanceTestSamples (r .name , self .samples )
336
382
r .samples .exclude_outliers ()
337
383
self .results .append (r )
384
+ r .yields = self .yields or None
338
385
self ._reset ()
339
386
340
387
def _store_memory_stats (self , max_rss , mem_pages ):
341
388
self .max_rss = int (max_rss )
342
389
self .mem_pages = int (mem_pages )
343
390
391
+ def _configure_format (self , header ):
392
+ self .quantiles = 'MEAN' not in header
393
+ self .memory = 'MAX_RSS' in header
394
+ self .delta = '𝚫' in header
395
+
344
396
# Regular expression and action to take when it matches the parsed line
345
397
state_actions = {
346
398
results_re : _append_result ,
@@ -355,6 +407,17 @@ def _store_memory_stats(self, max_rss, mem_pages):
355
407
self .samples .append (
356
408
Sample (int (i ), int (self .num_iters ), int (runtime )))),
357
409
410
+ re .compile (r'\s+SetUp (\d+)' ):
411
+ (lambda self , setup : setattr (self , 'setup' , int (setup ))),
412
+
413
+ re .compile (r'\s+Yielding after ~(\d+) μs' ):
414
+ (lambda self , since_last_yield :
415
+ self .yields .append (
416
+ Yield (len (self .samples ), int (since_last_yield )))),
417
+
418
+ re .compile (r'( *#[, \t]+TEST[, \t]+SAMPLES[, \t]+MIN.*)' ):
419
+ _configure_format ,
420
+
358
421
# Environmental statistics: memory usage and context switches
359
422
re .compile (r'\s+MAX_RSS \d+ - \d+ = (\d+) \((\d+) pages\)' ):
360
423
_store_memory_stats ,
0 commit comments