Package: exectiming
ExecTimingAn advanced timer for Python that makes it easy to determine execution times.Python has a built-in package namedtimeitwhich provides a way to
"Measure execution time of small code snippets." It can be great for quick
tests, but lacks more expansive features like curve fitting, statistical information,
and the ability to use in existing projects. ExecTiming seeks to change that by
including most of the features oftimeitand adding many more like decorators,
argument calling and replacement, best-fit-curve determination, and in-project use.FeaturesStaticTimerwhich provides the ability to time functions via a decorator,
strings, or just seeing the amount of time elapsed between a call to.start_elapsed()andelapsed()Timerwhich gives a way to log measured times and then display statistical data,
perform curve-fitting for functions with parameters as the independent variables and
the measured time as the dependent variable, and all the features the StaticTimer has.All output can be re-directed by changingoutput_stream, which can be any
file-like object or anything with a.write()method.A wrapper forlogging.infoandlogging.debugis included to redirect
output to those sourcesMeasured times can be displayed in secondss, millisecondsms,
microsecondsus, or nanosecondsns.The same block can be executed multiple times to get a more accurate reading.
This is done by settingiterations_per_run. After that many iterations, the
elapsed time is measured.Multiple runs can be carried out and averaged to remove outlying results.WikiGlossaryInstallationDecoratorTime StringsStatistics & Best Fit CurvePlottingInstallationpip install exectimingFor full functionality:scipy- for best-fit-curvescikit-learn- for best-fit-curvenumpy- for best-fit-curvematplotlib- for plottingHowever, basic functionality will still exist even if those dependencies aren't foundStatic decoratefromexectiming.exectimingimportStaticTimerfromrandomimportrandint@StaticTimer.decorate(runs=5,average_runs=False,call_callable_args=True,log_arguments=True)deffactorial(n):ifn==1:return1else:returnn*factorial.__wrapped__(n-1)factorial(lambda:randint(3,40))#    0.01663 ms - factorial(33) ... [runs=  1, iterations=  1] | Run 1#    0.00544 ms - factorial(19) ... [runs=  1, iterations=  1] | Run 2#    0.00736 ms - factorial(28) ... [runs=  1, iterations=  1] | Run 3#    0.00448 ms - factorial(17) ... [runs=  1, iterations=  1] | Run 4#    0.01087 ms - factorial(38) ... [runs=  1, iterations=  1] | Run 5Functions, even recursive ones, can be wrapped so that any time they are called they get timedYou can replace arguments with functions which can be automatically called and the argument replaced
to allow testing like shown above,call_callable_args=TrueArguments show up in the output to provide more information,log_arguments=TrueStatic time_itfromexectiming.exectimingimportStaticTimerStaticTimer.time_it("pow(2, 64)",runs=10,iterations_per_run=10000)#  107.10668 ms - pow(2, 64) ... [runs= 10, iterations=10000]StaticTimer.time_it("2**64",runs=10,iterations_per_run=10000)#   68.75266 ms - 2**64 ... [runs= 10, iterations=10000]StaticTimer.time_it("1<<64",runs=10,iterations_per_run=10000)#   65.53690 ms - 1<<64 ... [runs= 10, iterations=10000]Strings can be timedMultiple runs can be measured then averaged together to get a more accurate resultAnything needed names can be passed in by settingglobals=,locals=, orsetup=.
The first two must be maps of names to objects. The second is a string that is
executed once because the stringblockis timed.time_itcan be used to re-write the decorator above example like so:StaticTimer.time_it(factorial,lambda:randint(3,40),call_callable_args=True,average_runs=False,runs=5,log_arguments=True)Assumefromexectiming.exectimingimportTimertimer=Timer()Transformers, statistics, and best fit@timer.decorate(runs=5,log_arguments=True,call_callable_args=True)defbubble_sort(array):whileTrue:switched=Falseforiinrange(0,len(array)-1):ifarray[i]>array[i+1]:array[i],array[i+1]=array[i+1],array[i]switched=Trueifnotswitched:breakreturnarraybubble_sort(lambda:[randint(0,1000)for_inrange(randint(100,5000))])timer.output(split_index="bubble_sort",transformers={0:len})# bubble_sort:#     1333.19493 ms - bubble_sort(2141) ... [runs=  1, iterations=  1]#     1413.75243 ms - bubble_sort(2546) ... [runs=  1, iterations=  1]#     4247.70385 ms - bubble_sort(4530) ... [runs=  1, iterations=  1]#       34.01533 ms - bubble_sort(421)  ... [runs=  1, iterations=  1]#      675.07202 ms - bubble_sort(1752) ... [runs=  1, iterations=  1]timer.statistics()# bubble_sort[runs=5, total=7703.73856 ms]:#      Min | Max | Average = 34.01533 | 4247.70385 | 1540.74771 ms#       Standard Deviation = 1442.66797 ms#                 Variance = 2081.29087 msprint(timer.best_fit_curve(transformers=len))# ('Polynomial', {'a': 2.8042911992314363e-07, 'b': -9.670141667209306e-05, 'c': 0.024305228337961525})Timerstores the output until requestedFunction parameters can be transformed in the output. In the above example,transformers={0: len}indicates that the positional argument at index0should have its
value in the output replaced by the result of calling the function with that parameter.
The output otherwise would have been something likebubble_sort([1, 2, 3, 4, 5, ...])transformers=lenis also valid as all of the arguments can be transformed withlen, so
there is no need to specify which index/key the function should be used for.Basic statistics can be displayedA best-fit-curve can be determined. To use this, all logged function parameters
must be integers. In this case, one was an a list, so it was transformed so that the
analysis was done on the length of the list, instead of the list itself.The resulting best fit curve is, ifx=len(list),y = ax^2 + bx + c.
We can extrapolate execution time using this curve to determine how long it would
take to sort a list of lengthx=10000, which would be0.0000002804*10000^2 - 0.0000967*10000 + 0.0243. That is27.10 sor27100 msPlotting factorial@timer.decorate(runs=100,iterations_per_run=10,call_callable_args=True,log_arguments=True)deffactorial(n):ifn==1:return1else:returnn*factorial.__wrapped__(n-1)factorial(lambda:randint(1,100))timer.plot(plot_curve=True,time_unit=timer.US,equation_rounding=5).plot()provides a quick way to plot the measured times against an argument
that is the independent variableThe best fit curve and equation can be automatically determined and added
by settingplot_curve=TruePlotting bubble_sort# using bubble_sort from above, just with runs=10bubble_sort(lambda:[randint(0,100)for_inrange(randint(100,2500))])curve=timer.best_fit_curve(transformers=len)timer.plot(transformer=len,plot_curve=True,curve=curve,x_label="List Length")The curve can be determined beforehand and then passed intoplot()plot()needstransformer=lenbecause the independent and dependent variables
must be integers, solenis used to make it it onePlotting binary_search@timer.decorate(runs=100,iterations_per_run=5,log_arguments=True,call_callable_args=True)defbinary_search(sorted_array,element):lower,upper=0,len(sorted_array)middle=upper//2whilemiddle>=lowerandmiddle!=upper:ifelement==sorted_array[middle]:returnmiddleelifelement>sorted_array[middle]:lower=middle+1# lower must be beyond middle because the middle wasn't rightelse:upper=middle-1# upper must be lower than the middle because the middle wasn't rightmiddle=(upper+lower)//2returnNone# couldn't find itbinary_search(lambda:[iforiinrange(randint(0,10000))],lambda:randint(0,10000))timer.plot(plot_curve=True,curve=timer.best_fit_curve(exclude={1},transformers=len),key=0,transformer=len,time_unit=timer.US,x_label="List Length",equation_rounding=4,title="Binary Search - Random Size, Random Element")binary_search()takes two arguments, sobest_fit_curveis set to ignore
the second one, at index 1, and to transform the argument at index 0 usinglenOnce the curve is determined, the split must be plotted. Again, there are
two arguments, sokey=0says to use the first as the independent variable andtransformer=lenwill transform the list into an integerAdditionally, the title and x-axis labels are specified and rounding set lowerThe equation displayed on the plot uses the timescale specified bytime_unitPlotting multiple splitsfromexectiming.exectimingimportTimerfromrandomimportrandinttimer=Timer()defbubble_sort(array):whileTrue:switched=Falseforiinrange(0,len(array)-1):ifarray[i]>array[i+1]:array[i],array[i+1]=array[i+1],array[i]switched=Trueifnotswitched:breakreturnarraydefselection_sort(array):foriinrange(len(array)-1):# find minmin_i=iforjinrange(i+1,len(array)):ifarray[j]<array[min_i]:min_i=j# swaparray[i],array[min_i]=array[min_i],array[i]returnarraytimer.time_it(bubble_sort,lambda:[randint(0,1000)for_inrange(randint(100,3000))],call_callable_args=True,log_arguments=True,runs=10)timer.time_it(selection_sort,lambda:[randint(0,1000)for_inrange(randint(100,3000))],call_callable_args=True,log_arguments=True,runs=10)timer.time_it(sorted,lambda:[randint(0,1000)for_inrange(randint(100,3000))],call_callable_args=True,log_arguments=True,runs=10)bubble_sort_curve=timer.best_fit_curve("bubble_sort",transformers=len)selection_sort_curve=timer.best_fit_curve("selection_sort",transformers=len)sorted_curve=timer.best_fit_curve("sorted",transformers=len)timer.plot("bubble_sort",plot_curve=True,curve=bubble_sort_curve,multiple=True,transformer=len)timer.plot("selection_sort",plot_curve=True,curve=selection_sort_curve,multiple=True,transformer=len)timer.plot("sorted",plot_curve=True,curve=sorted_curve,title="Sorting Algorithms",x_label="List Length",transformer=len)Multiple splits and curves can be plotted by settingmultiple=Truefor
all but the last call to.plot().title,x_label, andy_labelof the last call will be usedTODOAdd parameter checking to.plot()and.best_fit_curve()to make sure
arguments are integers to avoid difficult to decipher errorsChange.best_fit_curve()to allowtransformersto be a callableChange.output()to not requiresplit_indexiftransformers={0:len}.
Allowtransformersto be just a function, if there is only one argument, or a map
or a map of a map.Change.sort_runs()to reflect that values don't have to be integers,
they just have to be comparable. If they aren't, then a transformer is needed.
This change is mainly cosmetic. (BJ - nothing actually needed changed)Add.predict(params, arguments)toTimer. Should basically be a
pass-through call to.calculate_point()on the correct best-fit-curveCollapseexclude_argsandexclude_kwargsdown into justexclude.
The difference between positional and keyword arguments can be determined as
int vs. str.Change how coefficients are returned forBestFitLinear, maybe usexindex/keyAdd context managerMake scipy, numpy, and scikit-learn optional, just prohibitbest_fit_curveif they aren't thereAdd graphing feature with matplotlib, Linear will only be graphed if there is a single argumentAdd the ability to sort runs so they are display in some sort of order. Maybe allow sorting
by time or by an argument