Package: about-time
about-timeA cool helper for tracking time and throughput of code blocks, with beautiful human friendly renditions.What does it do?Did you ever need to measure the duration of an operation? Yeah, this is easy.But how to:measure the duration of two or more blocks at the same time, including the whole duration?instrument a code to cleanly retrieve durations in one line, to log or send to time series databases?easily see human friendly durations ins(seconds),ms(milliseconds),µs(microseconds) and evenns(nanoseconds)?easily see human friendly counts with SI prefixes likek,M,G,T, etc?measure the actual throughput of a block? (this is way harder, since it needs to measure both duration and number of iterations)easily see human friendly throughputs in "/second", "/minute", "/hour" or even "/day", including SI prefixes?Yes, it can get tricky! More interesting details aboutdurationandthroughput.If you'd tried to do it without these magic, it would probably get messy and immensely pollute the code being instrumented.I have the solution, behold!fromabout_timeimportabout_timedefsome_func():importtimetime.sleep(85e-3)returnTruedefmain():withabout_time()ast1:# <-- use it like a context manager!t2=about_time(some_func)# <-- use it with any callable!!t3=about_time(x*2forxinrange(56789))# <-- use it with any iterable or generator!!!data=[xforxint3]# then just iterate!print(f'total:{t1.duration_human}')print(f'  some_func:{t2.duration_human}-> result:{t2.result}')print(f'  generator:{t3.duration_human}->{t3.count_human}elements, throughput:{t3.throughput_human}')Thismain()function prints:total: 95.6ms
  some_func: 89.7ms -> result: True
  generator: 5.79ms -> 56.8k elements, throughput: 9.81M/sHow cool is that? 😲👏You can also get the duration in seconds if needed:In [7]: t1.duration
Out[7]: 0.09556673200064251But95.6msis way better, isn't it? The same withcountandthroughput!So,about_timemeasures code blocks, both time and throughput, and converts them to beautiful human friendly representations! 👏Get itJust install with pip:❯pipinstallabout-timeUse itThere are three modes of operation: context manager, callable and throughput. Let's dive in.1. Use it like a context manager:fromabout_timeimportabout_timewithabout_time()ast:# the code to be measured...# any lenghty block.print(f'The whole block took:{t.duration_human}')This way you can nicely wrap any amount of code.In this mode, there are the basic fieldsdurationandduration_human.2. Use it with any callable:fromabout_timeimportabout_timet=about_time(some_func)print(f'The whole block took:{t.duration_human}')print(f'And the result was:{t.result}')This way you have a nice one liner, and do not need to increase the indent of your code.In this mode, there is an additional fieldresult, with the return of the callable.If the callable have params, you can use alambdaor (📌 new) simply send them:defadd(n,m):returnn+mt=about_time(add,1,41)# or:t=about_time(add,n=1,m=41)# or even:t=about_time(lambda:add(1,41))3. Use it with any iterable or generator:fromabout_timeimportabout_timet=about_time(iterable)foritemint:# process item.print(f'The whole block took:{t.duration_human}')print(f'It was detected{t.count_human}elements')print(f'The throughput was:{t.throughput_human}')This wayabout_timealso extracts the number of iterations, and with the measured duration it calculates the throughput of the whole loop! It's especially useful with generators, which do not have length.In this mode, there are the additional fieldscount,count_human,throughputandthroughput_human.Cool tricks under the hood:you can use it even with generator expressions, anything that is iterable to python!you can consume it not only in aforloop, but also in { list | dict | set } comprehensions,map()s,filter()s,sum()s,max()s,list()s, etc, thus any function that expects an iterator! 👏the timer only starts when the first element is queried, so you can initialize whatever you need before entering the loop! 👏thecount/count_humanandthroughput/throughput_humanfields are updated inreal time, so you can use them even inside the loop!Features:According to the SI standard, there are 1000 bytes in akilobyte.There is another standard called IEC that has 1024 bytes in akibibyte, but this is only useful when measuring things that are naturally a power of two, e.g. a stick of RAM.Be careful to not render IEC quantities with SI scaling, which would be incorrect. But I still support it, if you really want to ;)By default, this will use SI,1000divisor, andno spacebetween values and scales/units. SI uses prefixes:k,M,G,T,P,E,Z, andY.These are the optional features:iec=> use IEC instead of SI:Ki,Mi,Gi,Ti,Pi,Ei,Zi,Yi(implies1024);1024=> use1024divisor — ifiecis not enabled, use prefixes:K,M,G,T,P,E,Z, andY(note the upper 'K');space=> include a space between values and scales/units everywhere:48 Binstead of48B,15.6 µsinstead of15.6µs, and12.4 kB/sinstead of12.4kB/s.To change them, just use the properties:fromabout_timeimportFEATURESFEATURES.feature_1024FEATURES.feature_iecFEATURES.feature_spaceFor example, to enable spaces between scales/units:fromabout_timeimportFEATURESFEATURES.feature_space=TrueThe human duration magicI've used just one key concept in designing the human duration features: cleanliness.3.44sis more meaningful than3.43584783784s, and14.1usis much nicer than.0000141233333s.So what I do is: round values to at most two decimal places (three significant digits), and find the best scale unit to represent them, minimizing resulting values smaller than1. The search for the best unit considers even the rounding been applied!0.000999999does not end up as999.99us(truncate) nor1000.0us(bad unit), but is auto-upgraded to the next unit1.0ms!Theduration_humanunits change seamlessly from nanoseconds to hours.values smaller than 60 seconds are always rendered as "num.D[D]unit", with one or two decimals;from 1 minute onward it changes to "H:MM:SS".It feels much more humane, humm? ;)Some examples:duration (float seconds)duration_human.00000000185'1.85ns'.000000999996'1.00µs'.00001'10.0µs'.0000156'15.6µs'.01'10.0ms'.0141233333333'14.1ms'.1099999'110ms'.1599999'160ms'.8015'802ms'3.434999'3.43s'59.999'0:01:00'68.5'0:01:08'125.825'0:02:05'4488.395'1:14:48'The human throughput magicI've made thethroughput_humanwith a similar logic. It is funny how much trickier "throughput" is to the human brain!If something took1165263 secondsto handle123 items, how fast did it go? It's not obvious...It doesn't help even if we divide the duration by the number of items,9473 seconds/item, which still does not mean much. How fast was that? We can't say.How many items did we do per time unit?Oh, we just need to invert it, so0,000105555569858 items/second, there it is! 😂To make some sense of it we need to multiply that by 3600 (seconds in an hour) to get0.38/h, which is much better, and again by 24 (hours in a day) to finally get9.12/d!! Now we know how fast that process was! \o/ As you see, it's not easy at all.Thethroughput_humanunit changes seamlessly from per-second, per-minute, per-hour, and per-day.It also automatically inserts SI-prefixes, like k, M, and G. 👍duration (float seconds)number of elementsthroughput_human1.10'10.0/s'1.2500'2.50k/s'1.1825000'1.82M/s'2.1'30.0/m'2.10'5.00/s'1.98198198198198211'5.55/s'100.10'6.00/m'1600.3'6.75/h'.991'1.01/s'1165263.123'9.12/d'Accuracyabout_timesupports all versions of python, but in pythons >=3.3it performs even better, with much higher resolution and smaller propagation of errors, thanks to the newtime.perf_counter. In older versions, it usestime.timeas usual.Changelog highlights:4.2.1: makes fixed precision actually gain more resolution, when going from a default 1 to 2 decimals4.2.0: support for fixed precision, useful when one needs output without varying lengths; official Python 3.11 support4.1.0: enable to cache features within closures, to improve performance forhttps://github.com/rsalmei/alive-progress4.0.0: new version, modeled after my Rust implementation inhttps://crates.io/crates/human-repr; includes new global features, new objects for each operation, and especially, new simpler human friendly representations; supports Python 3.7+3.3.0: new interfaces for count_human and throughput_human; support more common Kbyte for base 2 (1024), leaving IEC one as an alternate3.2.2: support IEC kibibyte standard for base 2 (1024)3.2.1: support divisor in throughput_human3.2.0: both durations and throughputs now use 3 significant digits; throughputs now include SI-prefixes3.1.1: makeduration_human()andthroughput_human()available for external use3.1.0: include support for parameters in callable mode; official support for python 3.8, 3.9 and 3.103.0.0: greatly improved the counter/throughput mode, with a single argument and working in real time2.0.0: feature complete, addition of callable and throughput modes1.0.0: first public release, context manager modeLicenseThis software is licensed under the MIT License. See the LICENSE file in the top distribution directory for the full license text.Maintaining an open source project is hard and time-consuming, and I've put much ❤️ and effort into this.If you've appreciated my work, you can back me up with a donation! Thank you 😊