halib 0.1.75__py3-none-any.whl → 0.1.80__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
halib/research/perftb.py CHANGED
@@ -306,9 +306,18 @@ class PerfTB:
306
306
 
307
307
  # Save and open plot
308
308
  if save_path:
309
- fig.write_image(save_path, engine="kaleido")
309
+ export_success = False
310
+ try:
311
+ fig.write_image(save_path, engine="kaleido")
312
+ export_success = True
310
313
  # pprint(f"Saved: {os.path.abspath(save_path)}")
311
- if open_plot and os.name == "nt": # Windows
314
+ except Exception as e:
315
+ print(f"Error saving plot: {e}")
316
+ pprint(
317
+ "Failed to save plot. Check this link: https://stackoverflow.com/questions/69016568/unable-to-export-plotly-images-to-png-with-kaleido. Maybe you need to downgrade kaleido version to 0.1.* or install it via pip install kaleido==0.1.*"
318
+ )
319
+ return
320
+ if export_success and open_plot and os.name == "nt": # Windows
312
321
  os.system(f'start "" "{os.path.abspath(save_path)}"')
313
322
  return fig
314
323
 
@@ -0,0 +1,288 @@
1
+ import os
2
+ import time
3
+ import json
4
+
5
+ from pathlib import Path
6
+ from pprint import pprint
7
+ from threading import Lock
8
+
9
+ from plotly.subplots import make_subplots
10
+ import plotly.graph_objects as go
11
+ import plotly.express as px # for dynamic color scales
12
+
13
+ from loguru import logger
14
+
15
+ class zProfiler:
16
+ """A singleton profiler to measure execution time of contexts and steps.
17
+
18
+ Args:
19
+ interval_report (int): Frequency of periodic reports (0 to disable).
20
+ stop_to_view (bool): Pause execution to view reports if True (only in debug mode).
21
+ output_file (str): Path to save the profiling report.
22
+ report_format (str): Output format for reports ("json" or "csv").
23
+
24
+ Example:
25
+ prof = zProfiler()
26
+ prof.ctx_start("my_context")
27
+ prof.step_start("my_context", "step1")
28
+ time.sleep(0.1)
29
+ prof.step_end("my_context", "step1")
30
+ prof.ctx_end("my_context")
31
+ """
32
+
33
+ _instance = None
34
+ _lock = Lock()
35
+
36
+ def __new__(cls, *args, **kwargs):
37
+ with cls._lock:
38
+ if cls._instance is None:
39
+ cls._instance = super().__new__(cls)
40
+ return cls._instance
41
+
42
+ def __init__(
43
+ self,
44
+ ):
45
+ if not hasattr(self, "_initialized"):
46
+ self.time_dict = {}
47
+ self._initialized = True
48
+
49
+ def ctx_start(self, ctx_name="ctx_default"):
50
+ if not isinstance(ctx_name, str) or not ctx_name:
51
+ raise ValueError("ctx_name must be a non-empty string")
52
+ if ctx_name not in self.time_dict:
53
+ self.time_dict[ctx_name] = {
54
+ "start": time.perf_counter(),
55
+ "step_dict": {},
56
+ "report_count": 0,
57
+ }
58
+ self.time_dict[ctx_name]["report_count"] += 1
59
+
60
+ def ctx_end(self, ctx_name="ctx_default", report_func=None):
61
+ if ctx_name not in self.time_dict:
62
+ return
63
+ self.time_dict[ctx_name]["end"] = time.perf_counter()
64
+ self.time_dict[ctx_name]["duration"] = (
65
+ self.time_dict[ctx_name]["end"] - self.time_dict[ctx_name]["start"]
66
+ )
67
+
68
+ def step_start(self, ctx_name, step_name):
69
+ if not isinstance(step_name, str) or not step_name:
70
+ raise ValueError("step_name must be a non-empty string")
71
+ if ctx_name not in self.time_dict:
72
+ return
73
+ if step_name not in self.time_dict[ctx_name]["step_dict"]:
74
+ self.time_dict[ctx_name]["step_dict"][step_name] = []
75
+ self.time_dict[ctx_name]["step_dict"][step_name].append([time.perf_counter()])
76
+
77
+ def step_end(self, ctx_name, step_name):
78
+ if (
79
+ ctx_name not in self.time_dict
80
+ or step_name not in self.time_dict[ctx_name]["step_dict"]
81
+ ):
82
+ return
83
+ self.time_dict[ctx_name]["step_dict"][step_name][-1].append(time.perf_counter())
84
+
85
+ def _step_dict_to_detail(self, ctx_step_dict):
86
+ """
87
+ 'ctx_step_dict': {
88
+ │ │ 'preprocess': [
89
+ │ │ │ [278090.947465806, 278090.960484853],
90
+ │ │ │ [278091.178424035, 278091.230944486],
91
+ │ │ 'infer': [
92
+ │ │ │ [278090.960490534, 278091.178424035],
93
+ │ │ │ [278091.230944486, 278091.251378469],
94
+ │ }
95
+ """
96
+ assert (
97
+ len(ctx_step_dict.keys()) > 1
98
+ ), "step_dict must have only one key (step_name) for detail."
99
+
100
+ for step_name, time_list in ctx_step_dict.items():
101
+ normed_ctx_step_dict = {}
102
+ if not isinstance(ctx_step_dict[step_name], list):
103
+ raise ValueError(f"Step data for {step_name} must be a list")
104
+ step_name = list(ctx_step_dict.keys())[0]
105
+ normed_time_ls = []
106
+ for idx, time_data in enumerate(time_list):
107
+ elapsed_time = -1
108
+ if len(time_data) == 2:
109
+ start, end = time_data[0], time_data[1]
110
+ elapsed_time = end - start
111
+ normed_time_ls.append((idx, elapsed_time)) # including step
112
+ normed_ctx_step_dict[step_name] = normed_time_ls
113
+ return normed_ctx_step_dict
114
+
115
+ def get_report_dict(self, with_detail=False):
116
+ report_dict = {}
117
+ for ctx_name, ctx_dict in self.time_dict.items():
118
+ report_dict[ctx_name] = {
119
+ "duration": ctx_dict.get("duration", 0.0),
120
+ "step_dict": {
121
+ "summary": {"avg_time": {}, "percent_time": {}},
122
+ "detail": {},
123
+ },
124
+ }
125
+
126
+ if with_detail:
127
+ report_dict[ctx_name]["step_dict"]["detail"] = (
128
+ self._step_dict_to_detail(ctx_dict["step_dict"])
129
+ )
130
+ avg_time_list = []
131
+ epsilon = 1e-5
132
+ for step_name, step_list in ctx_dict["step_dict"].items():
133
+ durations = []
134
+ try:
135
+ for time_data in step_list:
136
+ if len(time_data) != 2:
137
+ continue
138
+ start, end = time_data
139
+ durations.append(end - start)
140
+ except Exception as e:
141
+ logger.error(
142
+ f"Error processing step {step_name} in context {ctx_name}: {e}"
143
+ )
144
+ continue
145
+ if not durations:
146
+ continue
147
+ avg_time = sum(durations) / len(durations)
148
+ if avg_time < epsilon:
149
+ continue
150
+ avg_time_list.append((step_name, avg_time))
151
+ total_avg_time = (
152
+ sum(time for _, time in avg_time_list) or 1e-10
153
+ ) # Avoid division by zero
154
+ for step_name, avg_time in avg_time_list:
155
+ report_dict[ctx_name]["step_dict"]["summary"]["percent_time"][
156
+ f"per_{step_name}"
157
+ ] = (avg_time / total_avg_time) * 100.0
158
+ report_dict[ctx_name]["step_dict"]["summary"]["avg_time"][
159
+ f"avg_{step_name}"
160
+ ] = avg_time
161
+ report_dict[ctx_name]["step_dict"]["summary"][
162
+ "total_avg_time"
163
+ ] = total_avg_time
164
+ report_dict[ctx_name]["step_dict"]["summary"] = dict(
165
+ sorted(report_dict[ctx_name]["step_dict"]["summary"].items())
166
+ )
167
+ return report_dict
168
+
169
+ @classmethod
170
+ @classmethod
171
+ def plot_formatted_data(
172
+ cls, profiler_data, outdir=None, file_format="png", do_show=False
173
+ ):
174
+ """
175
+ Plot each context in a separate figure with bar + pie charts.
176
+ Save each figure in the specified format (png or svg).
177
+ """
178
+
179
+ if outdir is not None:
180
+ os.makedirs(outdir, exist_ok=True)
181
+
182
+ if file_format.lower() not in ["png", "svg"]:
183
+ raise ValueError("file_format must be 'png' or 'svg'")
184
+
185
+ results = {} # {context: fig}
186
+
187
+ for ctx, ctx_data in profiler_data.items():
188
+ summary = ctx_data["step_dict"]["summary"]
189
+ avg_times = summary["avg_time"]
190
+ percent_times = summary["percent_time"]
191
+
192
+ step_names = [s.replace("avg_", "") for s in avg_times.keys()]
193
+ pprint(f'{step_names=}')
194
+ n_steps = len(step_names)
195
+
196
+ # Generate dynamic colors
197
+ colors = px.colors.sample_colorscale(
198
+ "Viridis", [i / (n_steps - 1) for i in range(n_steps)]
199
+ )
200
+ # pprint(f'{len(colors)} colors generated for {n_steps} steps')
201
+ color_map = dict(zip(step_names, colors))
202
+
203
+ # Create figure
204
+ fig = make_subplots(
205
+ rows=1,
206
+ cols=2,
207
+ subplot_titles=[f"Avg Time", f"% Time"],
208
+ specs=[[{"type": "bar"}, {"type": "pie"}]],
209
+ )
210
+
211
+ # Bar chart
212
+ fig.add_trace(
213
+ go.Bar(
214
+ x=step_names,
215
+ y=list(avg_times.values()),
216
+ text=[f"{v*1000:.2f} ms" for v in avg_times.values()],
217
+ textposition="outside",
218
+ marker=dict(color=[color_map[s] for s in step_names]),
219
+ name="", # unified legend
220
+ showlegend=False,
221
+ ),
222
+ row=1,
223
+ col=1,
224
+ )
225
+
226
+ # Pie chart (colors match bar)
227
+ fig.add_trace(
228
+ go.Pie(
229
+ labels=step_names,
230
+ values=list(percent_times.values()),
231
+ marker=dict(colors=[color_map[s] for s in step_names]),
232
+ hole=0.4,
233
+ name="",
234
+ showlegend=True,
235
+ ),
236
+ row=1,
237
+ col=2,
238
+ )
239
+
240
+ # Layout
241
+ fig.update_layout(
242
+ title_text=f"Context Profiler: {ctx}",
243
+ width=1000,
244
+ height=400,
245
+ showlegend=True,
246
+ legend=dict(title="Steps", x=1.05, y=0.5, traceorder="normal"),
247
+ hovermode="x unified",
248
+ )
249
+
250
+ fig.update_xaxes(title_text="Steps", row=1, col=1)
251
+ fig.update_yaxes(title_text="Avg Time (ms)", row=1, col=1)
252
+
253
+ # Show figure
254
+ if do_show:
255
+ fig.show()
256
+
257
+ # Save figure
258
+ if outdir is not None:
259
+ file_path = os.path.join(outdir, f"{ctx}_summary.{file_format.lower()}")
260
+ fig.write_image(file_path)
261
+ print(f"Saved figure: {file_path}")
262
+
263
+ results[ctx] = fig
264
+
265
+ return results
266
+
267
+ def report_and_plot(self, outdir=None, file_format="png", do_show=False):
268
+ """
269
+ Generate the profiling report and plot the formatted data.
270
+
271
+ Args:
272
+ outdir (str): Directory to save figures. If None, figures are only shown.
273
+ file_format (str): Target file format, "png" or "svg". Default is "png".
274
+ do_show (bool): Whether to display the plots. Default is False.
275
+ """
276
+ report = self.get_report_dict()
277
+ self.get_report_dict(with_detail=False)
278
+ return self.plot_formatted_data(
279
+ report, outdir=outdir, file_format=file_format, do_show=do_show
280
+ )
281
+
282
+ def save_report_dict(self, output_file, with_detail=False):
283
+ try:
284
+ report = self.get_report_dict(with_detail=with_detail)
285
+ with open(output_file, "w") as f:
286
+ json.dump(report, f, indent=4)
287
+ except Exception as e:
288
+ logger.error(f"Failed to save report to {output_file}: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halib
3
- Version: 0.1.75
3
+ Version: 0.1.80
4
4
  Summary: Small library for common tasks
5
5
  Author: Hoang Van Ha
6
6
  Author-email: hoangvanhauit@gmail.com
@@ -12,8 +12,12 @@ Description-Content-Type: text/markdown
12
12
  License-File: LICENSE.txt
13
13
  Requires-Dist: arrow
14
14
  Requires-Dist: click
15
+ Requires-Dist: dataclass-wizard
15
16
  Requires-Dist: enlighten
17
+ Requires-Dist: itables
18
+ Requires-Dist: kaleido
16
19
  Requires-Dist: loguru
20
+ Requires-Dist: matplotlib
17
21
  Requires-Dist: more-itertools
18
22
  Requires-Dist: moviepy
19
23
  Requires-Dist: networkx
@@ -22,25 +26,20 @@ Requires-Dist: omegaconf
22
26
  Requires-Dist: opencv-python
23
27
  Requires-Dist: pandas
24
28
  Requires-Dist: Pillow
29
+ Requires-Dist: plotly
25
30
  Requires-Dist: Pyarrow
26
31
  Requires-Dist: pycurl
32
+ Requires-Dist: pygwalker
27
33
  Requires-Dist: python-telegram-bot
28
34
  Requires-Dist: requests
29
35
  Requires-Dist: rich
30
36
  Requires-Dist: scikit-learn
31
- Requires-Dist: matplotlib
32
37
  Requires-Dist: seaborn
33
- Requires-Dist: plotly
34
- Requires-Dist: pygwalker
35
38
  Requires-Dist: tabulate
36
- Requires-Dist: itables
37
39
  Requires-Dist: timebudget
38
40
  Requires-Dist: tqdm
39
41
  Requires-Dist: tube_dl
40
42
  Requires-Dist: wandb
41
- Requires-Dist: dataclass-wizard
42
- Requires-Dist: kaleido==0.1.*; sys_platform == "win32"
43
- Requires-Dist: kaleido; sys_platform == "linux"
44
43
  Dynamic: author
45
44
  Dynamic: author-email
46
45
  Dynamic: classifier
@@ -53,7 +52,11 @@ Dynamic: summary
53
52
 
54
53
  Helper package for coding and automation
55
54
 
56
- **Version 0.1.74**
55
+ **Version 0.1.80**
56
+
57
+ + `research/profiler`: add `zProfiler` class to measure execution time of contexts and steps, with support for dynamic color scales in plots.
58
+
59
+ **Version 0.1.77**
57
60
 
58
61
  + `research/base_exp`: add base experiment class to handle common experiment tasks, including performance calculation and saving results.
59
62
 
@@ -34,8 +34,9 @@ halib/research/benchquery.py,sha256=FuKnbWQtCEoRRtJAfN-zaN-jPiO_EzsakmTOMiqi7GQ,
34
34
  halib/research/dataset.py,sha256=QU0Hr5QFb8_XlvnOMgC9QJGIpwXAZ9lDd0RdQi_QRec,6743
35
35
  halib/research/metrics.py,sha256=Xgv0GUGo-o-RJaBOmkRCRpQJaYijF_1xeKkyYU_Bv4U,5249
36
36
  halib/research/perfcalc.py,sha256=qDa0sqfpWrwGZVJtjuUVFK7JX6j8xyXP9OnnfYmdamg,15898
37
- halib/research/perftb.py,sha256=vazU-dYBJhfc4sK4zFgxOvzeXGi-5TyPHCt20ItiWhY,30463
37
+ halib/research/perftb.py,sha256=FWg0b8wSgy4UwuvHSXwEqvTq1Rhi-z-HtAKuQg1lWc4,30989
38
38
  halib/research/plot.py,sha256=-pDUk4z3C_GnyJ5zWmf-mGMdT4gaipVJWzIgcpIPiRk,9448
39
+ halib/research/profiler.py,sha256=4q2nRTr3OQRE_z6zsnb7k9epk66z_jlW1kgphykNmJ4,10937
39
40
  halib/research/torchloader.py,sha256=yqUjcSiME6H5W210363HyRUrOi3ISpUFAFkTr1w4DCw,6503
40
41
  halib/research/wandb_op.py,sha256=YzLEqME5kIRxi3VvjFkW83wnFrsn92oYeqYuNwtYRkY,4188
41
42
  halib/sys/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -51,8 +52,8 @@ halib/utils/gpu_mon.py,sha256=vD41_ZnmPLKguuq9X44SB_vwd9JrblO4BDzHLXZhhFY,2233
51
52
  halib/utils/listop.py,sha256=Vpa8_2fI0wySpB2-8sfTBkyi_A4FhoFVVvFiuvW8N64,339
52
53
  halib/utils/tele_noti.py,sha256=-4WXZelCA4W9BroapkRyIdUu9cUVrcJJhegnMs_WpGU,5928
53
54
  halib/utils/video.py,sha256=ZqzNVPgc1RZr_T0OlHvZ6SzyBpL7O27LtB86JMbBuR0,3059
54
- halib-0.1.75.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
55
- halib-0.1.75.dist-info/METADATA,sha256=Hic05vB3hcIpLbNKux1IsnlOUBMVa5smppGOdUQZUzQ,5778
56
- halib-0.1.75.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
- halib-0.1.75.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
58
- halib-0.1.75.dist-info/RECORD,,
55
+ halib-0.1.80.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
56
+ halib-0.1.80.dist-info/METADATA,sha256=6fPL11EXy7ahsWkxxUNf0jB4qAmH61ZLjHxejiLP7IA,5864
57
+ halib-0.1.80.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
+ halib-0.1.80.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
59
+ halib-0.1.80.dist-info/RECORD,,
File without changes