omdev 0.0.0.dev196__py3-none-any.whl → 0.0.0.dev198__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- omdev/.manifests.json +12 -0
- omdev/tools/linehisto.py +335 -0
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/RECORD +8 -7
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev196.dist-info → omdev-0.0.0.dev198.dist-info}/top_level.txt +0 -0
omdev/.manifests.json
CHANGED
@@ -302,6 +302,18 @@
|
|
302
302
|
}
|
303
303
|
}
|
304
304
|
},
|
305
|
+
{
|
306
|
+
"module": ".tools.linehisto",
|
307
|
+
"attr": "_CLI_MODULE",
|
308
|
+
"file": "omdev/tools/linehisto.py",
|
309
|
+
"line": 330,
|
310
|
+
"value": {
|
311
|
+
"$.cli.types.CliModule": {
|
312
|
+
"cmd_name": "linehisto",
|
313
|
+
"mod_name": "omdev.tools.linehisto"
|
314
|
+
}
|
315
|
+
}
|
316
|
+
},
|
305
317
|
{
|
306
318
|
"module": ".tools.mkrelimp",
|
307
319
|
"attr": "_CLI_MODULE",
|
omdev/tools/linehisto.py
ADDED
@@ -0,0 +1,335 @@
|
|
1
|
+
"""
|
2
|
+
TODO:
|
3
|
+
- cmd line options
|
4
|
+
- truncation
|
5
|
+
- mean/min/max/median/stddev
|
6
|
+
- delta mean/min/max/median/stddev
|
7
|
+
- heat - red rapidly changing blue stale
|
8
|
+
- paging
|
9
|
+
- find
|
10
|
+
- stdin/stdout redir (ttyname(0))
|
11
|
+
- graph
|
12
|
+
"""
|
13
|
+
import curses
|
14
|
+
import heapq
|
15
|
+
import operator
|
16
|
+
import sys
|
17
|
+
import time
|
18
|
+
import typing as ta
|
19
|
+
|
20
|
+
from ..cli import CliModule
|
21
|
+
|
22
|
+
|
23
|
+
##
|
24
|
+
|
25
|
+
|
26
|
+
def with_timer(interval=5.):
|
27
|
+
def outer(fn):
|
28
|
+
start = time.time()
|
29
|
+
last = start
|
30
|
+
|
31
|
+
def inner(*args, **kwargs):
|
32
|
+
nonlocal last
|
33
|
+
|
34
|
+
now = time.time()
|
35
|
+
if (now - last) < interval:
|
36
|
+
return None
|
37
|
+
|
38
|
+
last = now
|
39
|
+
return fn(*args, **kwargs)
|
40
|
+
|
41
|
+
return inner
|
42
|
+
|
43
|
+
return outer
|
44
|
+
|
45
|
+
|
46
|
+
def calc_percent(a: float, b: float) -> float:
|
47
|
+
if not a or not b:
|
48
|
+
return 0.
|
49
|
+
return ((a * 1e9) // (b * 1e5)) / 100.
|
50
|
+
|
51
|
+
|
52
|
+
##
|
53
|
+
|
54
|
+
|
55
|
+
class KeyedHisto:
|
56
|
+
def __init__(
|
57
|
+
self,
|
58
|
+
*,
|
59
|
+
max_entries: int | None = None,
|
60
|
+
) -> None:
|
61
|
+
super().__init__()
|
62
|
+
|
63
|
+
self.entries: dict[str, int] = {}
|
64
|
+
self.total_seen = 0
|
65
|
+
self.total_evicted = 0
|
66
|
+
|
67
|
+
self.max_entries = max_entries
|
68
|
+
if max_entries is not None:
|
69
|
+
self.eviction_len = int(max_entries * 2)
|
70
|
+
|
71
|
+
self.num_evicted = 0
|
72
|
+
|
73
|
+
def __len__(self) -> int:
|
74
|
+
return len(self.entries)
|
75
|
+
|
76
|
+
@property
|
77
|
+
def total_tracked(self) -> int:
|
78
|
+
return self.total_seen - self.total_evicted
|
79
|
+
|
80
|
+
def inc(self, key: str, n: int = 1) -> int:
|
81
|
+
if self.max_entries is not None and len(self) >= self.eviction_len:
|
82
|
+
self.evict(len(self) - self.max_entries)
|
83
|
+
|
84
|
+
self.total_seen += n
|
85
|
+
|
86
|
+
ct = self.entries.get(key, 0) + n
|
87
|
+
self.entries[key] = ct
|
88
|
+
|
89
|
+
return ct
|
90
|
+
|
91
|
+
@property
|
92
|
+
def items(self) -> ta.Iterable[tuple[str, int]]:
|
93
|
+
return self.entries.items()
|
94
|
+
|
95
|
+
@property
|
96
|
+
def sorted(self) -> list[tuple[str, int]]:
|
97
|
+
items = sorted(self.items, key=operator.itemgetter(1))
|
98
|
+
return items[::-1]
|
99
|
+
|
100
|
+
def evict(self, n: int = 1) -> None:
|
101
|
+
self.num_evicted += n
|
102
|
+
|
103
|
+
for key, ct in heapq.nsmallest(n, self.items, key=operator.itemgetter(1)):
|
104
|
+
self.total_evicted += ct
|
105
|
+
|
106
|
+
del self.entries[key]
|
107
|
+
|
108
|
+
def nlargest(self, n: int = 20) -> list[tuple[str, int]]:
|
109
|
+
return heapq.nlargest(n, self.items, key=operator.itemgetter(1))
|
110
|
+
|
111
|
+
def nsmallest(self, n: int = 20) -> list[tuple[str, int]]:
|
112
|
+
return heapq.nsmallest(n, self.items, key=operator.itemgetter(1))
|
113
|
+
|
114
|
+
|
115
|
+
class KeyedHistoRenderer:
|
116
|
+
def __init__(
|
117
|
+
self,
|
118
|
+
histo: KeyedHisto,
|
119
|
+
*,
|
120
|
+
max_lines: int | None = None,
|
121
|
+
) -> None:
|
122
|
+
super().__init__()
|
123
|
+
|
124
|
+
self.histo = histo
|
125
|
+
self.max_lines = max_lines
|
126
|
+
|
127
|
+
@property
|
128
|
+
def entries_to_render(self) -> list[tuple[str, int]]:
|
129
|
+
if self.max_lines is None:
|
130
|
+
return list(self.histo.sorted)
|
131
|
+
|
132
|
+
nlines = min(self.max_lines, len(self.histo))
|
133
|
+
|
134
|
+
return self.histo.nlargest(nlines)
|
135
|
+
|
136
|
+
def render_header(self, count_width: int) -> str:
|
137
|
+
header = 'count : % sen : % tkd : line'
|
138
|
+
|
139
|
+
if count_width > 5:
|
140
|
+
header = (' ' * (count_width - 5)) + header
|
141
|
+
|
142
|
+
return header
|
143
|
+
|
144
|
+
def render_entry(self, entry: tuple[str, int], count_width: int) -> str:
|
145
|
+
line, count = entry
|
146
|
+
|
147
|
+
line_fmt = '%' + str(count_width) + 'd : %5s : %5s : %s'
|
148
|
+
|
149
|
+
percent_seen_str = f'{calc_percent(count, self.histo.total_seen):3.2f}'
|
150
|
+
percent_tracked_str = f'{calc_percent(count, self.histo.total_tracked):3.2f}'
|
151
|
+
|
152
|
+
return line_fmt % (count, percent_seen_str, percent_tracked_str, line)
|
153
|
+
|
154
|
+
def render_entries(self, entries: ta.Iterable[tuple[str, int]], count_width: int) -> list[str]:
|
155
|
+
return [self.render_entry(entry, count_width) for entry in entries]
|
156
|
+
|
157
|
+
def render_status(self) -> str:
|
158
|
+
parts = [f'{self.histo.total_seen} total seen']
|
159
|
+
|
160
|
+
total_tracked_percent = calc_percent(self.histo.total_tracked, self.histo.total_seen)
|
161
|
+
parts.extend([
|
162
|
+
f'{len(self.histo)} tracked',
|
163
|
+
f'{self.histo.total_tracked} / {total_tracked_percent:.2f} % total tracked',
|
164
|
+
])
|
165
|
+
|
166
|
+
parts.extend([
|
167
|
+
f'{self.histo.num_evicted} evicted',
|
168
|
+
f'{self.histo.total_evicted} / {100.0 - total_tracked_percent:.2f} % total evicted',
|
169
|
+
])
|
170
|
+
|
171
|
+
duplicate_evictions = self.histo.total_evicted - self.histo.num_evicted
|
172
|
+
parts.append(
|
173
|
+
f'{duplicate_evictions} / {calc_percent(duplicate_evictions, self.histo.total_evicted):.2f} '
|
174
|
+
f'% duplicate evictions',
|
175
|
+
)
|
176
|
+
|
177
|
+
# tracked % + duplicate evicted %
|
178
|
+
parts.append(
|
179
|
+
f'{calc_percent(self.histo.total_tracked - duplicate_evictions, self.histo.total_seen):.2f} % correct',
|
180
|
+
)
|
181
|
+
|
182
|
+
return ', '.join(parts)
|
183
|
+
|
184
|
+
class Rendered(ta.NamedTuple):
|
185
|
+
status_line: str
|
186
|
+
header_line: str
|
187
|
+
entry_lines: list[str]
|
188
|
+
|
189
|
+
def render(self, entries: ta.Sequence[tuple[str, int]] | None = None) -> Rendered:
|
190
|
+
if entries is None:
|
191
|
+
entries = self.entries_to_render
|
192
|
+
|
193
|
+
max_count = entries[0][1] if entries else 0
|
194
|
+
count_width = len(str(max_count))
|
195
|
+
|
196
|
+
status_line = self.render_status()
|
197
|
+
header_line = self.render_header(count_width)
|
198
|
+
entry_lines = self.render_entries(entries, count_width)
|
199
|
+
|
200
|
+
return self.Rendered(
|
201
|
+
status_line,
|
202
|
+
header_line,
|
203
|
+
entry_lines,
|
204
|
+
)
|
205
|
+
|
206
|
+
def render_to_str(self) -> str:
|
207
|
+
status_line, header_line, entry_lines = self.render()
|
208
|
+
|
209
|
+
return '\n'.join([status_line, '', header_line, *entry_lines, ''])
|
210
|
+
|
211
|
+
|
212
|
+
class CursesKeyedHistoRenderer(KeyedHistoRenderer):
|
213
|
+
color_normal = 0
|
214
|
+
color_green = 1
|
215
|
+
color_yellow = 2
|
216
|
+
color_red = 3
|
217
|
+
|
218
|
+
def __init__(
|
219
|
+
self,
|
220
|
+
window,
|
221
|
+
histo: KeyedHisto,
|
222
|
+
*,
|
223
|
+
redraw_interval: float = .1,
|
224
|
+
) -> None:
|
225
|
+
self.window = window
|
226
|
+
self.redraw_interval = redraw_interval
|
227
|
+
|
228
|
+
h, w = self.window.getmaxyx()
|
229
|
+
max_lines = h - 3
|
230
|
+
|
231
|
+
self.max_line_len = w - 30
|
232
|
+
|
233
|
+
super().__init__(
|
234
|
+
histo,
|
235
|
+
max_lines=max_lines,
|
236
|
+
)
|
237
|
+
|
238
|
+
self.timed_redraw = with_timer(redraw_interval)(self.draw)
|
239
|
+
|
240
|
+
self.last_drawn_entries: list[tuple[str, int]] = []
|
241
|
+
|
242
|
+
curses.init_pair(self.color_green, curses.COLOR_GREEN, curses.COLOR_BLACK)
|
243
|
+
curses.init_pair(self.color_yellow, curses.COLOR_YELLOW, curses.COLOR_BLACK)
|
244
|
+
curses.init_pair(self.color_red, curses.COLOR_RED, curses.COLOR_BLACK)
|
245
|
+
|
246
|
+
def get_entry_color(
|
247
|
+
self,
|
248
|
+
cur_pos: int,
|
249
|
+
last_pos: int | None,
|
250
|
+
) -> int:
|
251
|
+
if last_pos is None:
|
252
|
+
return self.color_green
|
253
|
+
elif last_pos > cur_pos:
|
254
|
+
return self.color_yellow
|
255
|
+
elif last_pos < cur_pos:
|
256
|
+
return self.color_red
|
257
|
+
else:
|
258
|
+
return self.color_normal
|
259
|
+
|
260
|
+
def get_entry_colors(self, entries: ta.Iterable[tuple[str, int]]) -> list[int]:
|
261
|
+
last_pos_map = {s: i for i, (s, _) in enumerate(self.last_drawn_entries)}
|
262
|
+
|
263
|
+
return [
|
264
|
+
self.get_entry_color(i, last_pos_map.get(key))
|
265
|
+
for i, (key, _) in enumerate(entries)
|
266
|
+
]
|
267
|
+
|
268
|
+
def draw(self) -> None:
|
269
|
+
entries = self.entries_to_render
|
270
|
+
|
271
|
+
status_line, header_line, entry_lines = self.render(entries)
|
272
|
+
entry_colors = self.get_entry_colors(entries)
|
273
|
+
|
274
|
+
self.last_drawn_entries = entries
|
275
|
+
|
276
|
+
self.window.clear()
|
277
|
+
self.window.addstr(0, 0, status_line)
|
278
|
+
self.window.addstr(2, 0, header_line)
|
279
|
+
|
280
|
+
for i, (line, color) in enumerate(zip(entry_lines, entry_colors)):
|
281
|
+
self.window.addstr(i + 3, 0, line[:120], curses.color_pair(color))
|
282
|
+
|
283
|
+
self.window.refresh()
|
284
|
+
|
285
|
+
|
286
|
+
def main() -> None:
|
287
|
+
screen = curses.initscr()
|
288
|
+
curses.start_color()
|
289
|
+
curses.curs_set(0)
|
290
|
+
|
291
|
+
histo = KeyedHisto(max_entries=10_000)
|
292
|
+
renderer = CursesKeyedHistoRenderer(screen, histo)
|
293
|
+
|
294
|
+
try:
|
295
|
+
while True:
|
296
|
+
try:
|
297
|
+
line = sys.stdin.readline()
|
298
|
+
except UnicodeDecodeError:
|
299
|
+
# FIXME
|
300
|
+
continue
|
301
|
+
|
302
|
+
if not line:
|
303
|
+
break
|
304
|
+
|
305
|
+
if len(line) > renderer.max_line_len:
|
306
|
+
line = line[renderer.max_line_len:]
|
307
|
+
|
308
|
+
line = line.strip()
|
309
|
+
|
310
|
+
if not line:
|
311
|
+
continue
|
312
|
+
|
313
|
+
histo.inc(line)
|
314
|
+
renderer.timed_redraw()
|
315
|
+
|
316
|
+
# screen.nodelay(1)
|
317
|
+
# ch = screen.getch()
|
318
|
+
# if ch != curses.ERR:
|
319
|
+
# histo.inc(ch, 100)
|
320
|
+
|
321
|
+
except (OSError, KeyboardInterrupt):
|
322
|
+
pass
|
323
|
+
|
324
|
+
finally:
|
325
|
+
curses.endwin()
|
326
|
+
|
327
|
+
sys.stdout.write(KeyedHistoRenderer(histo).render_to_str())
|
328
|
+
|
329
|
+
|
330
|
+
# @omlish-manifest
|
331
|
+
_CLI_MODULE = CliModule('linehisto', __name__)
|
332
|
+
|
333
|
+
|
334
|
+
if __name__ == '__main__':
|
335
|
+
main()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: omdev
|
3
|
-
Version: 0.0.0.
|
3
|
+
Version: 0.0.0.dev198
|
4
4
|
Summary: omdev
|
5
5
|
Author: wrmsr
|
6
6
|
License: BSD-3-Clause
|
@@ -12,7 +12,7 @@ Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: Operating System :: POSIX
|
13
13
|
Requires-Python: >=3.12
|
14
14
|
License-File: LICENSE
|
15
|
-
Requires-Dist: omlish==0.0.0.
|
15
|
+
Requires-Dist: omlish==0.0.0.dev198
|
16
16
|
Provides-Extra: all
|
17
17
|
Requires-Dist: black~=24.10; extra == "all"
|
18
18
|
Requires-Dist: pycparser~=2.22; extra == "all"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
omdev/.manifests.json,sha256=
|
1
|
+
omdev/.manifests.json,sha256=u3arSHz2TkzWc1jB0u6MFRJVTmjXS0SRcBvslqSxTt8,8575
|
2
2
|
omdev/__about__.py,sha256=n5x-SO70OgbDQFzQ1d7sZDVMsnkQc4PxQZPFaIQFa0E,1281
|
3
3
|
omdev/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
4
|
omdev/bracepy.py,sha256=I8EdqtDvxzAi3I8TuMEW-RBfwXfqKbwp06CfOdj3L1o,2743
|
@@ -165,6 +165,7 @@ omdev/tools/doc.py,sha256=wvgGhv6aFaV-Zl-Qivejx37i-lKQ207rZ-4K2fPf-Ss,2547
|
|
165
165
|
omdev/tools/docker.py,sha256=KVFckA8eAdiapFUr8xkfMw9Uv3Qy4oNq0e70Lqt1F7I,7352
|
166
166
|
omdev/tools/git.py,sha256=gqbvt8FFiPsj3vrQ7TxUiq9FpYX2c9UZ54io2p3-x7w,7541
|
167
167
|
omdev/tools/importscan.py,sha256=nhJIhtjDY6eFVlReP7fegvv6L5ZjN-Z2VeyhsBonev4,4639
|
168
|
+
omdev/tools/linehisto.py,sha256=0ZNm34EuiZBE9Q2YC6KNLNNydNT8QPSOwvYzXiU9S2Q,8881
|
168
169
|
omdev/tools/mkrelimp.py,sha256=kyu_BbUakKHEEOxNEvYWk7tH1ixCfVb3NqqT8U-BozE,4066
|
169
170
|
omdev/tools/notebook.py,sha256=q1YMGwM1skHv-dPbtT_cM7UOGFNiMEAxjr6rr6rbobk,3494
|
170
171
|
omdev/tools/pip.py,sha256=eBD41hp-V3thGfhUBM3Erxl4CSG-5LG6Szo1sA76P2k,3459
|
@@ -182,9 +183,9 @@ omdev/tools/json/rendering.py,sha256=tMcjOW5edfozcMSTxxvF7WVTsbYLoe9bCKFh50qyaGw
|
|
182
183
|
omdev/tools/pawk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
183
184
|
omdev/tools/pawk/__main__.py,sha256=VCqeRVnqT1RPEoIrqHFSu4PXVMg4YEgF4qCQm90-eRI,66
|
184
185
|
omdev/tools/pawk/pawk.py,sha256=Eckymn22GfychCQcQi96BFqRo_LmiJ-EPhC8TTUJdB4,11446
|
185
|
-
omdev-0.0.0.
|
186
|
-
omdev-0.0.0.
|
187
|
-
omdev-0.0.0.
|
188
|
-
omdev-0.0.0.
|
189
|
-
omdev-0.0.0.
|
190
|
-
omdev-0.0.0.
|
186
|
+
omdev-0.0.0.dev198.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
|
187
|
+
omdev-0.0.0.dev198.dist-info/METADATA,sha256=vkKKIgZLZ2Zb_sdrluJx4URchzeBCHiu9AyvFJ-SjNY,1760
|
188
|
+
omdev-0.0.0.dev198.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
189
|
+
omdev-0.0.0.dev198.dist-info/entry_points.txt,sha256=dHLXFmq5D9B8qUyhRtFqTGWGxlbx3t5ejedjrnXNYLU,33
|
190
|
+
omdev-0.0.0.dev198.dist-info/top_level.txt,sha256=1nr7j30fEWgLYHW3lGR9pkdHkb7knv1U1ES1XRNVQ6k,6
|
191
|
+
omdev-0.0.0.dev198.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|