11from unittest .mock import patch
22from io import StringIO
33import time
4- import sys
5-
6- MAX_LOOP = 10 ** 8
7-
8- def to_time_str (sec ):
9- if sec >= 60 :
10- return '{:.f}m' .fomat (sec / 60 )
11- elif sec >= 1 :
12- return '{:.5f}s' .format (sec )
13- elif sec >= 1e-3 :
14- return '{:.5f}ms' .format (sec * 1e3 )
15- elif sec >= 1e-6 :
16- return '{:.5f}µs' .format (sec * 1e6 )
17- else :
18- return '{:.5f}ns' .format (sec * 1e9 )
19-
20- def _print_avg_time (elapse_time , num_iter ):
21- avg_time = elapse_time / num_iter
22- print ('\n ==> avg time: {} in {} iterations' .format (to_time_str (avg_time ), num_iter ), file = sys .stderr , end = '' )
23-
24- def check_elapse_time (func , args , expected = None , num_iter = 1 ):
25- st = time .time ()
26- if expected :
27- for _ in range (num_iter ):
28- assert func (* args ) == expected
29- else :
30- for _ in range (num_iter ):
31- assert func (* args )
32- elapse_time = time .time () - st
33- _print_avg_time (elapse_time , num_iter )
4+ import datetime
5+ import os
6+ import subprocess
7+ from typing import List , Callable , Iterable
348
9+ from deprecation import deprecated
10+ from line_profiler import LineProfiler
3511
36- def get_output_with_stdin (func , inputs , num_iter = 1 , check_time = False ):
37- st = time .time ()
38- for _ in range (num_iter ):
39- with patch ("sys.stdin" , StringIO (inputs )), patch ("sys.stdout" , new_callable = StringIO ) as mocked_out :
12+ from util .cc_tools import eprint , get_caller_filename
13+ from util .prob_generate import list_to_string , merge_to_lines
14+
15+ MAX_LOOP = 10 ** 8
16+ BINARY_DIR_BASE = os .path .join (os .path .dirname (
17+ os .path .abspath (__file__ )), '../ext_code' )
18+
19+
20+ def logging_profile_info (msg_list , log_file_name ):
21+ out_lines = '\n ' + '#' * 120 + '\n '
22+ in_lines = '\n ' + '-' * 120 + '\n '
23+
24+ cur_time_str = str (datetime .datetime .now ())
25+ core_msg = in_lines .join ([f'{ name } : \n { msg } ' for name , msg in msg_list ])
26+ total_msg = out_lines .join (['' , cur_time_str , core_msg , '' ])
27+ with open (log_file_name , 'a' ) as f :
28+ f .write (total_msg )
29+ eprint (f'Profile is saved in { log_file_name } (appended)' )
30+
31+
32+ def timeit_lp (func , func_args , funcs = None , num_iter = 100 , time_limit = 0.1 , log = False ,
33+ silence = False , log_file_name = None , changed = '' , omit_func_args = False ):
34+ """execute timeit and line profiling
35+ :param func:
36+ :param func_args:
37+ :param funcs:
38+ :param num_iter:
39+ :param time_limit:
40+ :param silence: Dot not display stats by stderr
41+ :param log: additionally logging stats into log_file
42+ :param log_file_name: if log_file_name is not given, default value is caller's `filename.lprof`
43+ :param changed:
44+ :param omit_func_args:
45+ """
46+ timeit_msg = timeit (func , func_args , num_iter = num_iter ,
47+ time_limit = time_limit , return_msg = True , silence = silence )
48+ lp_msg = lprun (func , func_args , funcs = funcs ,
49+ return_msg = True , silence = silence )
50+ if log :
51+ log_file_name = log_file_name or get_caller_filename () + '.lprof'
52+ logging_profile_info ([
53+ ('changed' , changed ),
54+ ('args' , repr (func_args ) if not omit_func_args else '' ),
55+ ('timeit' , timeit_msg ),
56+ ('line_profiler' , lp_msg ),
57+ ], log_file_name )
58+
59+
60+ def lprun (func , args , funcs = None , silence = False , return_msg = False ):
61+ funcs = funcs or []
62+ if isinstance (args , str ) or not isinstance (args , Iterable ):
63+ args = (args , )
64+
65+ lp = LineProfiler ()
66+ for f in funcs :
67+ lp .add_function (f )
68+ lp_wrapper = lp (func )
69+ lp_wrapper (* args )
70+
71+ output = StringIO ()
72+ lp .print_stats (stream = output )
73+ msg = output .getvalue ()
74+ if not silence :
75+ eprint (msg )
76+ if return_msg :
77+ return msg
78+
79+
80+ def timeit (func , func_args , num_iter = 100 , time_limit = 0.1 , silence = False , return_msg = False ):
81+ elapse_times = _calc_elapse_times (
82+ func , func_args , num_iter = num_iter , time_limit = time_limit )
83+ msg = make_elapse_time_msg (elapse_times )
84+ if not silence :
85+ eprint (msg )
86+ if return_msg :
87+ return msg
88+
89+
90+ def timeits (func , func_args_list , num_iter = 100 , time_limit_per_args = 0.1 , silence = False , return_msg = False ):
91+ elapse_times = []
92+ for func_args in func_args_list :
93+ e_times = _calc_elapse_times (
94+ func , func_args , num_iter = num_iter , time_limit = time_limit_per_args )
95+ elapse_times .extend (e_times )
96+ msg = make_elapse_time_msg (elapse_times )
97+ if not silence :
98+ eprint (msg )
99+ if return_msg :
100+ return msg
101+
102+
103+ def time_complexity (func : Callable , args_func : Callable , scales : Iterable , num_iter = 10 , time_limit = 1 ):
104+ elapse_times = []
105+ for scale in scales :
106+ args = args_func (scale )
107+ cur_elapse_times = _calc_elapse_times (
108+ func , args , num_iter = num_iter , time_limit = time_limit )
109+ avg_time = sum (cur_elapse_times ) / len (cur_elapse_times )
110+ elapse_times .append (avg_time )
111+ return {'scale' : scales , 'elapse_time' : elapse_times }
112+
113+
114+ def compare_func_result (func1 , func2 , args , silence = False ):
115+ try :
116+ res1 = func1 (* args )
117+ except Exception as ex :
118+ res1 = '<<<<<<<<<<< Error1({}) >>>>>>>>>>>>>>>' .format (ex )
119+ try :
120+ res2 = func2 (* args )
121+ except Exception as ex :
122+ res2 = '<<<<<<<<<<< Error2({}) >>>>>>>>>>>>>>>' .format (ex )
123+
124+ if res1 != res2 :
125+ if not silence :
126+ lines = [
127+ '=================== in_str ==================' ,
128+ repr (args ),
129+ '=================== func1 ==================' ,
130+ res1 ,
131+ '=================== func2 ==================' ,
132+ res2 ,
133+ '===============================================' ,
134+ ]
135+ eprint (merge_to_lines (lines ))
136+ raise Exception ("Match Failed" )
137+
138+
139+ def compare_func_results (func1 , func2 , args_iter , silence = False ):
140+ for args in args_iter :
141+ compare_func_result (func1 , func2 , args , silence = silence )
142+
143+
144+ def ext_binary_to_func (binary_path , binary_dir = BINARY_DIR_BASE ) -> Callable :
145+ def func (in_str ):
146+ res = subprocess .check_output (binary_path , input = in_str .encode ())
147+ return res .decode ().strip ()
148+
149+ binary_path = os .path .join (binary_dir , binary_path )
150+ return func
151+
152+
153+ def mock_io (func ):
154+ def mocked_func (stdin_str ):
155+ with patch ("sys.stdin" , StringIO (stdin_str )), patch ("sys.stdout" , new_callable = StringIO ) as mocked_out :
156+ func ()
157+ return mocked_out .getvalue ().strip ()
158+ return mocked_func
159+
160+
161+ @deprecated (details = 'use mock_io' )
162+ def io_mock (func ):
163+ return mock_io (func )
164+
165+
166+ @deprecated ('Use io_mock instead' )
167+ def evaluate_via_io (func , in_str ):
168+ return _evaluate (func , in_str , io_mock = True )
169+
170+
171+ ################################################################################
172+ ################################################################################
173+
174+
175+ @deprecated (details = 'Use io_mock instead' )
176+ def _evaluate (func , func_args , io_mock = False ):
177+ """evaluate func with func_args, to use io_mock easily"""
178+ if io_mock :
179+ in_str = func_args if isinstance (func_args , str ) else func_args [0 ]
180+ with patch ("sys.stdin" , StringIO (in_str )), patch ("sys.stdout" , new_callable = StringIO ) as mocked_out :
40181 func ()
41- elapse_time = time .time () - st
42- if check_time :
43- _print_avg_time (elapse_time , num_iter )
44- return mocked_out .getvalue ().strip ()
182+ return mocked_out .getvalue ().strip ()
183+ else :
184+ return func (* func_args )
185+
186+
187+ def _calc_elapse_times (func , func_args , num_iter = 1 , time_limit = None ) -> List [float ]:
188+ """execute func and return elapse times (and print out to stderr)"""
189+ elapse_times = []
190+ if isinstance (func_args , str ) or not isinstance (func_args , Iterable ):
191+ func_args = (func_args , )
192+ total_time = 0
193+ for _ in range (num_iter ):
194+ st = time .time ()
195+ func (* func_args )
196+ elapse_time = time .time () - st
197+ elapse_times .append (elapse_time )
198+ total_time += elapse_time
199+ if time_limit and total_time > time_limit :
200+ break
201+ return elapse_times
202+
203+
204+ def _to_time_str (sec ):
205+ if sec >= 60 :
206+ return '{:.2f}m' .format (sec / 60 )
207+ elif sec >= 1 :
208+ return '{:.2f}s' .format (sec )
209+ elif sec >= 1e-3 :
210+ return '{:.2f}ms' .format (sec * 1e3 )
211+ elif sec >= 1e-6 :
212+ return '{:.2f}µs' .format (sec * 1e6 )
213+ else :
214+ return '{:.2f}ns' .format (sec * 1e9 )
215+
216+
217+ def make_elapse_time_msg (elapse_times ):
218+ avg_time = sum (elapse_times ) / len (elapse_times )
219+ msg = '==> avg_time: {} in {} iterations' .format (
220+ _to_time_str (avg_time ), len (elapse_times ))
221+ return msg
0 commit comments