halib 0.2.30__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/__init__.py +94 -0
- halib/common/__init__.py +0 -0
- halib/common/common.py +326 -0
- halib/common/rich_color.py +285 -0
- halib/common.py +151 -0
- halib/csvfile.py +48 -0
- halib/cuda.py +39 -0
- halib/dataset.py +209 -0
- halib/exp/__init__.py +0 -0
- halib/exp/core/__init__.py +0 -0
- halib/exp/core/base_config.py +167 -0
- halib/exp/core/base_exp.py +147 -0
- halib/exp/core/param_gen.py +170 -0
- halib/exp/core/wandb_op.py +117 -0
- halib/exp/data/__init__.py +0 -0
- halib/exp/data/dataclass_util.py +41 -0
- halib/exp/data/dataset.py +208 -0
- halib/exp/data/torchloader.py +165 -0
- halib/exp/perf/__init__.py +0 -0
- halib/exp/perf/flop_calc.py +190 -0
- halib/exp/perf/gpu_mon.py +58 -0
- halib/exp/perf/perfcalc.py +470 -0
- halib/exp/perf/perfmetrics.py +137 -0
- halib/exp/perf/perftb.py +778 -0
- halib/exp/perf/profiler.py +507 -0
- halib/exp/viz/__init__.py +0 -0
- halib/exp/viz/plot.py +754 -0
- halib/filesys.py +117 -0
- halib/filetype/__init__.py +0 -0
- halib/filetype/csvfile.py +192 -0
- halib/filetype/ipynb.py +61 -0
- halib/filetype/jsonfile.py +19 -0
- halib/filetype/textfile.py +12 -0
- halib/filetype/videofile.py +266 -0
- halib/filetype/yamlfile.py +87 -0
- halib/gdrive.py +179 -0
- halib/gdrive_mkdir.py +41 -0
- halib/gdrive_test.py +37 -0
- halib/jsonfile.py +22 -0
- halib/listop.py +13 -0
- halib/online/__init__.py +0 -0
- halib/online/gdrive.py +229 -0
- halib/online/gdrive_mkdir.py +53 -0
- halib/online/gdrive_test.py +50 -0
- halib/online/projectmake.py +131 -0
- halib/online/tele_noti.py +165 -0
- halib/plot.py +301 -0
- halib/projectmake.py +115 -0
- halib/research/__init__.py +0 -0
- halib/research/base_config.py +100 -0
- halib/research/base_exp.py +157 -0
- halib/research/benchquery.py +131 -0
- halib/research/core/__init__.py +0 -0
- halib/research/core/base_config.py +144 -0
- halib/research/core/base_exp.py +157 -0
- halib/research/core/param_gen.py +108 -0
- halib/research/core/wandb_op.py +117 -0
- halib/research/data/__init__.py +0 -0
- halib/research/data/dataclass_util.py +41 -0
- halib/research/data/dataset.py +208 -0
- halib/research/data/torchloader.py +165 -0
- halib/research/dataset.py +208 -0
- halib/research/flop_csv.py +34 -0
- halib/research/flops.py +156 -0
- halib/research/metrics.py +137 -0
- halib/research/mics.py +74 -0
- halib/research/params_gen.py +108 -0
- halib/research/perf/__init__.py +0 -0
- halib/research/perf/flop_calc.py +190 -0
- halib/research/perf/gpu_mon.py +58 -0
- halib/research/perf/perfcalc.py +363 -0
- halib/research/perf/perfmetrics.py +137 -0
- halib/research/perf/perftb.py +778 -0
- halib/research/perf/profiler.py +301 -0
- halib/research/perfcalc.py +361 -0
- halib/research/perftb.py +780 -0
- halib/research/plot.py +758 -0
- halib/research/profiler.py +300 -0
- halib/research/torchloader.py +162 -0
- halib/research/viz/__init__.py +0 -0
- halib/research/viz/plot.py +754 -0
- halib/research/wandb_op.py +116 -0
- halib/rich_color.py +285 -0
- halib/sys/__init__.py +0 -0
- halib/sys/cmd.py +8 -0
- halib/sys/filesys.py +124 -0
- halib/system/__init__.py +0 -0
- halib/system/_list_pc.csv +6 -0
- halib/system/cmd.py +8 -0
- halib/system/filesys.py +164 -0
- halib/system/path.py +106 -0
- halib/tele_noti.py +166 -0
- halib/textfile.py +13 -0
- halib/torchloader.py +162 -0
- halib/utils/__init__.py +0 -0
- halib/utils/dataclass_util.py +40 -0
- halib/utils/dict.py +317 -0
- halib/utils/dict_op.py +9 -0
- halib/utils/gpu_mon.py +58 -0
- halib/utils/list.py +17 -0
- halib/utils/listop.py +13 -0
- halib/utils/slack.py +86 -0
- halib/utils/tele_noti.py +166 -0
- halib/utils/video.py +82 -0
- halib/videofile.py +139 -0
- halib-0.2.30.dist-info/METADATA +237 -0
- halib-0.2.30.dist-info/RECORD +110 -0
- halib-0.2.30.dist-info/WHEEL +5 -0
- halib-0.2.30.dist-info/licenses/LICENSE.txt +17 -0
- halib-0.2.30.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from pprint import pprint
|
|
6
|
+
from threading import Lock
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
# Plotting libraries
|
|
11
|
+
from plotly.subplots import make_subplots
|
|
12
|
+
import plotly.graph_objects as go
|
|
13
|
+
import plotly.express as px # for dynamic color scales
|
|
14
|
+
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
|
|
17
|
+
from ...common.common import ConsoleLog
|
|
18
|
+
from ...system.path import *
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ==========================================
|
|
22
|
+
# 1. The Decorator
|
|
23
|
+
# ==========================================
|
|
24
|
+
def check_enabled(func):
|
|
25
|
+
"""
|
|
26
|
+
Decorator to skip method execution if the profiler is disabled.
|
|
27
|
+
|
|
28
|
+
This acts as a 'guard clause' for the entire function. If the profiler
|
|
29
|
+
instance has 'enabled=False', the decorated function is not executed at all,
|
|
30
|
+
saving processing time and avoiding side effects.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@wraps(func)
|
|
34
|
+
def wrapper(self, *args, **kwargs):
|
|
35
|
+
# Gracefully handle cases where 'enabled' might not be set yet (default to True)
|
|
36
|
+
# print('check_enabled called')
|
|
37
|
+
if not getattr(self, "enabled", True):
|
|
38
|
+
# print('Profiler disabled, skipping function execution.')
|
|
39
|
+
return # Exit immediately, returning None
|
|
40
|
+
return func(self, *args, **kwargs)
|
|
41
|
+
|
|
42
|
+
return wrapper
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ==========================================
|
|
46
|
+
# 2. The Class
|
|
47
|
+
# ==========================================
|
|
48
|
+
class ContextScope:
|
|
49
|
+
"""Helper to remember the current context name so you don't have to repeat it."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, profiler, ctx_name):
|
|
52
|
+
self.profiler = profiler
|
|
53
|
+
self.ctx_name = ctx_name
|
|
54
|
+
|
|
55
|
+
@contextmanager
|
|
56
|
+
def step(self, step_name):
|
|
57
|
+
# Automatically passes the stored ctx_name + the new step_name
|
|
58
|
+
with self.profiler.measure(self.ctx_name, step_name):
|
|
59
|
+
yield
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class zProfiler:
|
|
63
|
+
"""A singleton profiler to measure execution time of contexts and steps.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
interval_report (int): Frequency of periodic reports (0 to disable).
|
|
67
|
+
stop_to_view (bool): Pause execution to view reports if True (only in debug mode).
|
|
68
|
+
output_file (str): Path to save the profiling report.
|
|
69
|
+
report_format (str): Output format for reports ("json" or "csv").
|
|
70
|
+
|
|
71
|
+
Example (using context manager):
|
|
72
|
+
prof = zProfiler()
|
|
73
|
+
with prof.measure("my_context") as ctx:
|
|
74
|
+
with ctx.step("step1"):
|
|
75
|
+
time.sleep(0.1)
|
|
76
|
+
with ctx.step("step2"):
|
|
77
|
+
time.sleep(0.2)
|
|
78
|
+
|
|
79
|
+
Example (using raw methods):
|
|
80
|
+
prof = zProfiler()
|
|
81
|
+
prof.ctx_start("my_context")
|
|
82
|
+
prof.step_start("my_context", "step1")
|
|
83
|
+
time.sleep(0.1)
|
|
84
|
+
prof.step_end("my_context", "step1")
|
|
85
|
+
prof.ctx_end("my_context")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
_instance = None
|
|
91
|
+
_lock = Lock()
|
|
92
|
+
|
|
93
|
+
def __new__(cls, *args, **kwargs):
|
|
94
|
+
with cls._lock:
|
|
95
|
+
if cls._instance is None:
|
|
96
|
+
cls._instance = super().__new__(cls)
|
|
97
|
+
return cls._instance
|
|
98
|
+
|
|
99
|
+
def __init__(self, enabled=None):
|
|
100
|
+
"""
|
|
101
|
+
Args:
|
|
102
|
+
enabled (bool, optional):
|
|
103
|
+
- If True/False: Updates the enabled state immediately.
|
|
104
|
+
- If None: Keeps the current state (defaults to True on first init).
|
|
105
|
+
"""
|
|
106
|
+
# 1. First-time initialization
|
|
107
|
+
if not hasattr(self, "_initialized"):
|
|
108
|
+
self.enabled = enabled if enabled is not None else True
|
|
109
|
+
self.time_dict = {}
|
|
110
|
+
self._initialized = True
|
|
111
|
+
|
|
112
|
+
# 2. If initialized, allow updating 'enabled' ONLY if explicitly passed
|
|
113
|
+
elif enabled is not None:
|
|
114
|
+
self.enabled = enabled
|
|
115
|
+
|
|
116
|
+
@check_enabled
|
|
117
|
+
def ctx_start(self, ctx_name="ctx_default"):
|
|
118
|
+
if not isinstance(ctx_name, str) or not ctx_name:
|
|
119
|
+
raise ValueError("ctx_name must be a non-empty string")
|
|
120
|
+
if ctx_name not in self.time_dict:
|
|
121
|
+
self.time_dict[ctx_name] = {
|
|
122
|
+
"start": time.perf_counter(),
|
|
123
|
+
"step_dict": {},
|
|
124
|
+
"report_count": 0,
|
|
125
|
+
}
|
|
126
|
+
self.time_dict[ctx_name]["report_count"] += 1
|
|
127
|
+
|
|
128
|
+
@check_enabled
|
|
129
|
+
def ctx_end(self, ctx_name="ctx_default", report_func=None):
|
|
130
|
+
if ctx_name not in self.time_dict:
|
|
131
|
+
return
|
|
132
|
+
self.time_dict[ctx_name]["end"] = time.perf_counter()
|
|
133
|
+
self.time_dict[ctx_name]["duration"] = (
|
|
134
|
+
self.time_dict[ctx_name]["end"] - self.time_dict[ctx_name]["start"]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@check_enabled
|
|
138
|
+
def step_start(self, ctx_name, step_name):
|
|
139
|
+
if not isinstance(step_name, str) or not step_name:
|
|
140
|
+
raise ValueError("step_name must be a non-empty string")
|
|
141
|
+
if ctx_name not in self.time_dict:
|
|
142
|
+
return
|
|
143
|
+
if step_name not in self.time_dict[ctx_name]["step_dict"]:
|
|
144
|
+
self.time_dict[ctx_name]["step_dict"][step_name] = []
|
|
145
|
+
self.time_dict[ctx_name]["step_dict"][step_name].append([time.perf_counter()])
|
|
146
|
+
|
|
147
|
+
@check_enabled
|
|
148
|
+
def step_end(self, ctx_name, step_name):
|
|
149
|
+
if (
|
|
150
|
+
ctx_name not in self.time_dict
|
|
151
|
+
or step_name not in self.time_dict[ctx_name]["step_dict"]
|
|
152
|
+
):
|
|
153
|
+
return
|
|
154
|
+
self.time_dict[ctx_name]["step_dict"][step_name][-1].append(time.perf_counter())
|
|
155
|
+
|
|
156
|
+
@contextmanager
|
|
157
|
+
def measure(self, ctx_name, step_name=None):
|
|
158
|
+
if step_name is None:
|
|
159
|
+
# --- Context Mode ---
|
|
160
|
+
self.ctx_start(ctx_name)
|
|
161
|
+
try:
|
|
162
|
+
# Yield the helper object initialized with the current context name
|
|
163
|
+
yield ContextScope(self, ctx_name)
|
|
164
|
+
finally:
|
|
165
|
+
self.ctx_end(ctx_name)
|
|
166
|
+
else:
|
|
167
|
+
# --- Step Mode ---
|
|
168
|
+
self.step_start(ctx_name, step_name)
|
|
169
|
+
try:
|
|
170
|
+
yield
|
|
171
|
+
finally:
|
|
172
|
+
self.step_end(ctx_name, step_name)
|
|
173
|
+
|
|
174
|
+
def _step_dict_to_detail(self, ctx_step_dict):
|
|
175
|
+
"""
|
|
176
|
+
'ctx_step_dict': {
|
|
177
|
+
│ │ 'preprocess': [
|
|
178
|
+
│ │ │ [278090.947465806, 278090.960484853],
|
|
179
|
+
│ │ │ [278091.178424035, 278091.230944486],
|
|
180
|
+
│ │ 'infer': [
|
|
181
|
+
│ │ │ [278090.960490534, 278091.178424035],
|
|
182
|
+
│ │ │ [278091.230944486, 278091.251378469],
|
|
183
|
+
│ }
|
|
184
|
+
"""
|
|
185
|
+
assert len(ctx_step_dict.keys()) > 0, (
|
|
186
|
+
"step_dict must have only one key (step_name) for detail."
|
|
187
|
+
)
|
|
188
|
+
normed_ctx_step_dict = {}
|
|
189
|
+
for step_name, time_list in ctx_step_dict.items():
|
|
190
|
+
if not isinstance(ctx_step_dict[step_name], list):
|
|
191
|
+
raise ValueError(f"Step data for {step_name} must be a list")
|
|
192
|
+
# step_name = list(ctx_step_dict.keys())[0] # ! debug
|
|
193
|
+
normed_time_ls = []
|
|
194
|
+
for idx, time_data in enumerate(time_list):
|
|
195
|
+
elapsed_time = -1
|
|
196
|
+
if len(time_data) == 2:
|
|
197
|
+
start, end = time_data[0], time_data[1]
|
|
198
|
+
elapsed_time = end - start
|
|
199
|
+
normed_time_ls.append((idx, elapsed_time)) # including step
|
|
200
|
+
normed_ctx_step_dict[step_name] = normed_time_ls
|
|
201
|
+
return normed_ctx_step_dict
|
|
202
|
+
|
|
203
|
+
def get_report_dict(self, with_detail=False):
|
|
204
|
+
report_dict = {}
|
|
205
|
+
for ctx_name, ctx_dict in self.time_dict.items():
|
|
206
|
+
report_dict[ctx_name] = {
|
|
207
|
+
"duration": ctx_dict.get("duration", 0.0),
|
|
208
|
+
"step_dict": {
|
|
209
|
+
"summary": {"avg_time": {}, "percent_time": {}},
|
|
210
|
+
"detail": {},
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if with_detail:
|
|
215
|
+
report_dict[ctx_name]["step_dict"]["detail"] = (
|
|
216
|
+
self._step_dict_to_detail(ctx_dict["step_dict"])
|
|
217
|
+
)
|
|
218
|
+
avg_time_list = []
|
|
219
|
+
epsilon = 1e-5
|
|
220
|
+
for step_name, step_list in ctx_dict["step_dict"].items():
|
|
221
|
+
durations = []
|
|
222
|
+
try:
|
|
223
|
+
for time_data in step_list:
|
|
224
|
+
if len(time_data) != 2:
|
|
225
|
+
continue
|
|
226
|
+
start, end = time_data
|
|
227
|
+
durations.append(end - start)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(
|
|
230
|
+
f"Error processing step {step_name} in context {ctx_name}: {e}"
|
|
231
|
+
)
|
|
232
|
+
continue
|
|
233
|
+
if not durations:
|
|
234
|
+
continue
|
|
235
|
+
avg_time = sum(durations) / len(durations)
|
|
236
|
+
if avg_time < epsilon:
|
|
237
|
+
continue
|
|
238
|
+
avg_time_list.append((step_name, avg_time))
|
|
239
|
+
total_avg_time = (
|
|
240
|
+
sum(time for _, time in avg_time_list) or 1e-10
|
|
241
|
+
) # Avoid division by zero
|
|
242
|
+
for step_name, avg_time in avg_time_list:
|
|
243
|
+
report_dict[ctx_name]["step_dict"]["summary"]["percent_time"][
|
|
244
|
+
f"per_{step_name}"
|
|
245
|
+
] = (avg_time / total_avg_time) * 100.0
|
|
246
|
+
report_dict[ctx_name]["step_dict"]["summary"]["avg_time"][
|
|
247
|
+
f"avg_{step_name}"
|
|
248
|
+
] = avg_time
|
|
249
|
+
report_dict[ctx_name]["step_dict"]["summary"]["total_avg_time"] = (
|
|
250
|
+
total_avg_time
|
|
251
|
+
)
|
|
252
|
+
report_dict[ctx_name]["step_dict"]["summary"] = dict(
|
|
253
|
+
sorted(report_dict[ctx_name]["step_dict"]["summary"].items())
|
|
254
|
+
)
|
|
255
|
+
return report_dict
|
|
256
|
+
|
|
257
|
+
def get_report_dataframes(self):
|
|
258
|
+
"""
|
|
259
|
+
Returns two pandas DataFrames containing profiling data.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
tuple: (df_summary, df_detail)
|
|
263
|
+
- df_summary: Aggregated stats (Context, Step, Avg, %, Count)
|
|
264
|
+
- df_detail: Raw duration for every single iteration
|
|
265
|
+
"""
|
|
266
|
+
try:
|
|
267
|
+
import pandas as pd
|
|
268
|
+
except ImportError:
|
|
269
|
+
logger.error(
|
|
270
|
+
"Pandas is required for get_pandas_dfs(). Please pip install pandas."
|
|
271
|
+
)
|
|
272
|
+
return None, None
|
|
273
|
+
|
|
274
|
+
# Get full data structure
|
|
275
|
+
data = self.get_report_dict(with_detail=True)
|
|
276
|
+
|
|
277
|
+
summary_rows = []
|
|
278
|
+
detail_rows = []
|
|
279
|
+
|
|
280
|
+
for ctx_name, ctx_data in data.items():
|
|
281
|
+
summary = ctx_data["step_dict"]["summary"]
|
|
282
|
+
detail_dict = ctx_data["step_dict"]["detail"]
|
|
283
|
+
|
|
284
|
+
# --- 1. Build Summary Data ---
|
|
285
|
+
# Iterate keys in 'avg_time' to ensure we capture all steps
|
|
286
|
+
for avg_key, avg_val in summary["avg_time"].items():
|
|
287
|
+
step_name = avg_key.replace("avg_", "")
|
|
288
|
+
|
|
289
|
+
# Get corresponding percent
|
|
290
|
+
per_key = f"per_{step_name}"
|
|
291
|
+
percent_val = summary["percent_time"].get(per_key, 0.0)
|
|
292
|
+
|
|
293
|
+
# Get sample count from detail list length
|
|
294
|
+
count = len(detail_dict.get(step_name, []))
|
|
295
|
+
|
|
296
|
+
summary_rows.append(
|
|
297
|
+
{
|
|
298
|
+
"context_name": ctx_name,
|
|
299
|
+
"step_name": step_name,
|
|
300
|
+
"avg_time_sec": avg_val,
|
|
301
|
+
"percent_total": percent_val,
|
|
302
|
+
"sample_count": count,
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# --- 2. Build Detail Data ---
|
|
307
|
+
for step_name, time_list in detail_dict.items():
|
|
308
|
+
# time_list format: [(idx, duration), (idx, duration)...]
|
|
309
|
+
for item in time_list:
|
|
310
|
+
if len(item) == 2:
|
|
311
|
+
idx, duration = item
|
|
312
|
+
detail_rows.append(
|
|
313
|
+
{
|
|
314
|
+
"context_name": ctx_name,
|
|
315
|
+
"step_name": step_name,
|
|
316
|
+
"iteration_idx": idx,
|
|
317
|
+
"duration_sec": duration,
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Create DataFrames
|
|
322
|
+
df_summary = pd.DataFrame(summary_rows)
|
|
323
|
+
df_detail = pd.DataFrame(detail_rows)
|
|
324
|
+
|
|
325
|
+
# Reorder columns for readability (optional but nice)
|
|
326
|
+
if not df_summary.empty:
|
|
327
|
+
df_summary = df_summary[
|
|
328
|
+
[
|
|
329
|
+
"context_name",
|
|
330
|
+
"step_name",
|
|
331
|
+
"avg_time_sec",
|
|
332
|
+
"percent_total",
|
|
333
|
+
"sample_count",
|
|
334
|
+
]
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
if not df_detail.empty:
|
|
338
|
+
df_detail = df_detail[
|
|
339
|
+
["context_name", "step_name", "iteration_idx", "duration_sec"]
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
return df_summary, df_detail
|
|
343
|
+
|
|
344
|
+
def get_report_csv_files(self, outdir, tag="profiler"):
|
|
345
|
+
"""
|
|
346
|
+
Exports profiling data to two CSV files:
|
|
347
|
+
1. {tag}_summary.csv: Aggregated stats (Avg time, %)
|
|
348
|
+
2. {tag}_detailed_logs.csv: Raw duration for every iteration
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
outdir (str): Directory to save files.
|
|
352
|
+
tag (str): Optional prefix for filenames.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
if not os.path.exists(outdir):
|
|
356
|
+
os.makedirs(outdir)
|
|
357
|
+
tag_str = f"{tag}_" if tag else ""
|
|
358
|
+
summary_file = os.path.join(outdir, f"{tag_str}summary.csv")
|
|
359
|
+
detail_file = os.path.join(outdir, f"{tag_str}detailed_logs.csv")
|
|
360
|
+
|
|
361
|
+
df_summary, df_detail = self.get_report_dataframes()
|
|
362
|
+
if df_summary is not None:
|
|
363
|
+
df_summary.to_csv(summary_file, index=False, sep=";", encoding="utf-8")
|
|
364
|
+
logger.info(f"Saved summary CSV to: {summary_file}")
|
|
365
|
+
if df_detail is not None:
|
|
366
|
+
df_detail.to_csv(detail_file, index=False, sep=";", encoding="utf-8")
|
|
367
|
+
logger.info(f"Saved detailed logs CSV to: {detail_file}")
|
|
368
|
+
|
|
369
|
+
@classmethod
|
|
370
|
+
def plot_formatted_data(
|
|
371
|
+
cls, profiler_data, outdir=None, file_format="png", do_show=False, tag=""
|
|
372
|
+
):
|
|
373
|
+
"""
|
|
374
|
+
Plot each context in a separate figure with bar + pie charts.
|
|
375
|
+
Save each figure in the specified format (png or svg).
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
if outdir is not None:
|
|
379
|
+
os.makedirs(outdir, exist_ok=True)
|
|
380
|
+
|
|
381
|
+
if file_format.lower() not in ["png", "svg"]:
|
|
382
|
+
raise ValueError("file_format must be 'png' or 'svg'")
|
|
383
|
+
|
|
384
|
+
results = {} # {context: fig}
|
|
385
|
+
|
|
386
|
+
for ctx, ctx_data in profiler_data.items():
|
|
387
|
+
summary = ctx_data["step_dict"]["summary"]
|
|
388
|
+
avg_times = summary["avg_time"]
|
|
389
|
+
percent_times = summary["percent_time"]
|
|
390
|
+
|
|
391
|
+
step_names = [s.replace("avg_", "") for s in avg_times.keys()]
|
|
392
|
+
# pprint(f'{step_names=}')
|
|
393
|
+
n_steps = len(step_names)
|
|
394
|
+
|
|
395
|
+
assert n_steps > 0, "No steps found for context: {}".format(ctx)
|
|
396
|
+
# Generate dynamic colors
|
|
397
|
+
colors = (
|
|
398
|
+
px.colors.sample_colorscale(
|
|
399
|
+
"Viridis", [i / (n_steps - 1) for i in range(n_steps)]
|
|
400
|
+
)
|
|
401
|
+
if n_steps > 1
|
|
402
|
+
else [px.colors.sample_colorscale("Viridis", [0])[0]]
|
|
403
|
+
)
|
|
404
|
+
# pprint(f'{len(colors)} colors generated for {n_steps} steps')
|
|
405
|
+
color_map = dict(zip(step_names, colors))
|
|
406
|
+
|
|
407
|
+
# Create figure
|
|
408
|
+
fig = make_subplots(
|
|
409
|
+
rows=1,
|
|
410
|
+
cols=2,
|
|
411
|
+
subplot_titles=[f"Avg Time", f"% Time"],
|
|
412
|
+
specs=[[{"type": "bar"}, {"type": "pie"}]],
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Bar chart
|
|
416
|
+
fig.add_trace(
|
|
417
|
+
go.Bar(
|
|
418
|
+
x=step_names,
|
|
419
|
+
y=list(avg_times.values()),
|
|
420
|
+
text=[f"{v * 1000:.2f} ms" for v in avg_times.values()],
|
|
421
|
+
textposition="outside",
|
|
422
|
+
marker=dict(color=[color_map[s] for s in step_names]),
|
|
423
|
+
name="", # unified legend
|
|
424
|
+
showlegend=False,
|
|
425
|
+
),
|
|
426
|
+
row=1,
|
|
427
|
+
col=1,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Pie chart (colors match bar)
|
|
431
|
+
fig.add_trace(
|
|
432
|
+
go.Pie(
|
|
433
|
+
labels=step_names,
|
|
434
|
+
values=list(percent_times.values()),
|
|
435
|
+
marker=dict(colors=[color_map[s] for s in step_names]),
|
|
436
|
+
hole=0.4,
|
|
437
|
+
name="",
|
|
438
|
+
showlegend=True,
|
|
439
|
+
),
|
|
440
|
+
row=1,
|
|
441
|
+
col=2,
|
|
442
|
+
)
|
|
443
|
+
tag_str = tag if tag and len(tag) > 0 else ""
|
|
444
|
+
# Layout
|
|
445
|
+
fig.update_layout(
|
|
446
|
+
title_text=f"[{tag_str}] Context Profiler: {ctx}",
|
|
447
|
+
width=1000,
|
|
448
|
+
height=400,
|
|
449
|
+
showlegend=True,
|
|
450
|
+
legend=dict(title="Steps", x=1.05, y=0.5, traceorder="normal"),
|
|
451
|
+
hovermode="x unified",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
fig.update_xaxes(title_text="Steps", row=1, col=1)
|
|
455
|
+
fig.update_yaxes(title_text="Avg Time (ms)", row=1, col=1)
|
|
456
|
+
|
|
457
|
+
# Show figure
|
|
458
|
+
if do_show:
|
|
459
|
+
fig.show()
|
|
460
|
+
|
|
461
|
+
# Save figure
|
|
462
|
+
if outdir is not None:
|
|
463
|
+
file_prefix = ctx if len(tag_str) == 0 else f"{tag_str}_{ctx}"
|
|
464
|
+
file_path = os.path.join(
|
|
465
|
+
outdir, f"{file_prefix}_summary.{file_format.lower()}"
|
|
466
|
+
)
|
|
467
|
+
fig.write_image(file_path)
|
|
468
|
+
pprint(f"Saved figure to: 🔽")
|
|
469
|
+
pprint_local_path(file_path)
|
|
470
|
+
|
|
471
|
+
results[ctx] = fig
|
|
472
|
+
|
|
473
|
+
return results
|
|
474
|
+
|
|
475
|
+
def report_and_plot(self, outdir=None, file_format="png", do_show=False, tag=""):
|
|
476
|
+
"""
|
|
477
|
+
Generate the profiling report and plot the formatted data.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
outdir (str): Directory to save figures. If None, figures are only shown.
|
|
481
|
+
file_format (str): Target file format, "png" or "svg". Default is "png".
|
|
482
|
+
do_show (bool): Whether to display the plots. Default is False.
|
|
483
|
+
"""
|
|
484
|
+
report = self.get_report_dict()
|
|
485
|
+
self.get_report_dict(with_detail=False)
|
|
486
|
+
return self.plot_formatted_data(
|
|
487
|
+
report, outdir=outdir, file_format=file_format, do_show=do_show, tag=tag
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
def meta_info(self):
|
|
491
|
+
"""
|
|
492
|
+
Print the structure of the profiler's time dictionary.
|
|
493
|
+
Useful for debugging and understanding the profiler's internal state.
|
|
494
|
+
"""
|
|
495
|
+
for ctx_name, ctx_dict in self.time_dict.items():
|
|
496
|
+
with ConsoleLog(f"Context: {ctx_name}"):
|
|
497
|
+
step_names = list(ctx_dict["step_dict"].keys())
|
|
498
|
+
for step_name in step_names:
|
|
499
|
+
pprint(f"Step: {step_name}")
|
|
500
|
+
|
|
501
|
+
def save_report_dict(self, output_file, with_detail=False):
|
|
502
|
+
try:
|
|
503
|
+
report = self.get_report_dict(with_detail=with_detail)
|
|
504
|
+
with open(output_file, "w") as f:
|
|
505
|
+
json.dump(report, f, indent=4)
|
|
506
|
+
except Exception as e:
|
|
507
|
+
logger.error(f"Failed to save report to {output_file}: {e}")
|
|
File without changes
|