triordinal-dtree 2.0.0__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.
@@ -0,0 +1,6 @@
1
+ """DTree: incremental encoding of directed trees by two linear orders Q1 and Q2 (part of Triordinal)."""
2
+
3
+ from triordinal.dtree.core import SINK, SOURCE, DList, DNode, DTree, UMax, UMin, VMax, VMin
4
+
5
+ __all__ = ["DTree", "DNode", "DList", "VMin", "VMax", "UMin", "UMax", "SOURCE", "SINK"]
6
+ __version__ = "2.0.0"
@@ -0,0 +1,582 @@
1
+ """Performance benchmarks for DTree.
2
+
3
+ Usage, after ``pip install triordinal-dtree`` (or from the repository's python/ folder,
4
+ where ``python benchmarks/bench.py`` does the same):
5
+
6
+ python -m triordinal.dtree.bench # sizes 10,000 and 100,000
7
+ python -m triordinal.dtree.bench 1000 1000000 # custom sizes
8
+ python -m triordinal.dtree.bench --quick # small sizes, a few seconds
9
+ python -m triordinal.dtree.bench --dynamic # only the dynamic updates and the tree view
10
+ python -m triordinal.dtree.bench --note "idle machine after restart" # remark for the report
11
+ python -m triordinal.dtree.bench --no-save # do not write a report
12
+
13
+ Every run also writes its tables as a Markdown report, named after date and time
14
+ (e.g. 2026-10-17_14-18.md), with a header giving the DTree and Python versions, the
15
+ operating system and the processor: to benchmarks/python/ at the repository root when
16
+ run from a clone, otherwise to benchmarks/python/ in the current directory.
17
+
18
+ For each tree shape it reports the build time and how often positions in Q1
19
+ and Q2 had to be renumbered, then times the queries and structural changes.
20
+ Finally it compares reachability with the parent walk of classical doubly linked
21
+ trees, for single pairs and for and/or queries (VMin, VMax, UMin, UMax), over increasing depth,
22
+ and under dynamic updates, with insertions and queries alternating (including static
23
+ pre/post numbers recomputed after each insertion), and in a use case: a tree view whose
24
+ nodes are inserted, collapsed and expanded while rows are shown.
25
+ Timings depend on the machine; the renumbering counts do not.
26
+ """
27
+
28
+ import argparse
29
+ import datetime
30
+ import io
31
+ import os
32
+ import platform
33
+ import random
34
+ import sys
35
+ import time
36
+
37
+ import triordinal.dtree as _pkg
38
+ from triordinal.dtree import SINK, SOURCE, DList, DTree
39
+
40
+
41
+ def _base_dir():
42
+ """The repository root when run from a clone, otherwise the current directory."""
43
+ d = os.path.dirname(os.path.abspath(__file__))
44
+ for _ in range(6): # python/dtree/triordinal/dtree/bench.py -> repository root
45
+ if os.path.isdir(os.path.join(d, "csharp")) and os.path.isdir(os.path.join(d, "python")):
46
+ return d
47
+ d = os.path.dirname(d)
48
+ return os.getcwd()
49
+
50
+
51
+ REPO = _base_dir()
52
+ REPORTS = os.path.join(REPO, "benchmarks", "python")
53
+
54
+
55
+ class _Tee(io.TextIOBase):
56
+ """Writes to the console and keeps a copy for the report."""
57
+
58
+ def __init__(self, console):
59
+ self.console, self.copy = console, io.StringIO()
60
+
61
+ def write(self, text):
62
+ self.console.write(text)
63
+ self.console.flush()
64
+ self.copy.write(text)
65
+ return len(text)
66
+
67
+ def flush(self):
68
+ self.console.flush()
69
+
70
+
71
+ def _processor():
72
+ name = platform.processor() or platform.machine()
73
+ try: # Linux: the model name is more telling than the architecture
74
+ with open("/proc/cpuinfo", encoding="utf-8") as fh:
75
+ for line in fh:
76
+ if line.startswith("model name"):
77
+ return line.split(":", 1)[1].strip()
78
+ except OSError:
79
+ pass
80
+ return name
81
+
82
+
83
+ def write_report(body, started, args_text, note):
84
+ os.makedirs(REPORTS, exist_ok=True)
85
+ path = os.path.join(REPORTS, started.strftime("%Y-%m-%d_%H-%M") + ".md")
86
+ head = [
87
+ f"# DTree benchmarks, Python—{started.strftime('%Y-%m-%d %H:%M')}",
88
+ "",
89
+ f"- DTree (triordinal-dtree) {_pkg.__version__}, Python {sys.version.split()[0]} ({platform.python_implementation()})",
90
+ f"- {platform.system()} {platform.release()} ({platform.platform()})",
91
+ f"- Processor: {_processor()}, {os.cpu_count()} logical cores",
92
+ f"- Command: `python -m triordinal.dtree.bench{(' ' + args_text) if args_text else ''}`",
93
+ ]
94
+ if note:
95
+ head.append(f"- Note: {note}")
96
+ lines = body.splitlines()
97
+ if lines and lines[0].startswith("DTree benchmarks"):
98
+ lines = lines[1:] # the console title is replaced by the header above
99
+ with open(path, "w", encoding="utf-8") as fh:
100
+ fh.write("\n".join(head) + "\n" + "\n".join(lines).rstrip() + "\n")
101
+ return path
102
+
103
+ SHAPES = {
104
+ # name: function(i, names, rnd) -> parent of the new node n{i}
105
+ "random": lambda i, names, rnd: rnd.choice(names),
106
+ "chain": lambda i, names, rnd: names[-1],
107
+ "star (root)": lambda i, names, rnd: "n0",
108
+ "star (inner node)": lambda i, names, rnd: "n1",
109
+ # worst case for rational positions: always the newer half-gap, alternating sides
110
+ "alternating": lambda i, names, rnd: f"n{2 * ((i - 1) // 2)}",
111
+ # a chain of 3,000, then new leaves under the spine nodes from the top down, repeatedly:
112
+ # finding the end of a subtree by walking down the last children would be O(depth) here
113
+ "top-down comb": lambda i, names, rnd: names[-1] if i < 3000 else f"n{(i - 3000) % 3000}",
114
+ }
115
+
116
+
117
+ def build(shape, n, seed=1):
118
+ rnd = random.Random(seed)
119
+ t, names = DTree("B"), ["n0"]
120
+ pick = SHAPES[shape]
121
+ start = time.perf_counter()
122
+ for i in range(1, n):
123
+ if shape == "star (inner node)" and i <= 2:
124
+ parent = "n0" # n1 and n2 under the root, so n1 has a right neighbor
125
+ else:
126
+ parent = pick(i, names, rnd)
127
+ t.insert(parent, f"n{i}")
128
+ names.append(f"n{i}")
129
+ return t, names, time.perf_counter() - start
130
+
131
+
132
+ def stats(q):
133
+ return q.renumber_count, q.renumber_moved
134
+
135
+
136
+ def row(*cells):
137
+ print("| " + " | ".join(str(c) for c in cells) + " |")
138
+
139
+
140
+ def bench_build(sizes):
141
+ print("\n### Building trees\n")
142
+ row("shape", "nodes", "time [s]", "renumberings Q1 / Q2", "positions moved per insertion")
143
+ row("---", "---:", "---:", "---:", "---:")
144
+ for shape in SHAPES:
145
+ for n in sizes:
146
+ t, _, dt = build(shape, n)
147
+ c1, m1 = stats(t.Q1)
148
+ c2, m2 = stats(t.Q2)
149
+ row(shape, f"{n:,}", f"{dt:.2f}", f"{c1:,} / {c2:,}", f"{(m1 + m2) / (2 * n):.2f}")
150
+
151
+
152
+ def bench_lists(sizes):
153
+ print("\n### Growing a single list (DList)\n")
154
+ row("pattern", "nodes", "time [s]", "renumberings")
155
+ row("---", "---:", "---:", "---:")
156
+ for pattern in ("append", "prepend", "alternate"):
157
+ for n in sizes:
158
+ q = DList("Q")
159
+ start = time.perf_counter()
160
+ for i in range(n):
161
+ right = pattern == "append" or (pattern == "alternate" and i % 2)
162
+ if right:
163
+ q.insert_before(f"v{i}", SINK)
164
+ else:
165
+ q.insert_after(f"v{i}", SOURCE)
166
+ row(pattern, f"{n:,}", f"{time.perf_counter() - start:.2f}", f"{q.renumber_count:,}")
167
+
168
+
169
+ def bench_operations(n, queries=1_000_000):
170
+ print(f"\n### Queries and changes on a random tree of {n:,} nodes\n")
171
+ row("operation", "count", "time [s]", "per operation [µs]")
172
+ row("---", "---:", "---:", "---:")
173
+ rnd = random.Random(2)
174
+
175
+ def timed(name, count, fn):
176
+ start = time.perf_counter()
177
+ fn()
178
+ dt = time.perf_counter() - start
179
+ row(name, f"{count:,}", f"{dt:.2f}", f"{dt / count * 1e6:.1f}")
180
+
181
+ t, names, _ = build("random", n)
182
+ pairs = [(rnd.choice(names), rnd.choice(names)) for _ in range(queries)]
183
+ timed("can_reach (names)", queries, lambda: sum(t.can_reach(u, v) for u, v in pairs))
184
+ handles = [(t.get_node(u), t.get_node(v)) for u, v in pairs]
185
+ timed("can_reach (handles)", queries, lambda: sum(t.can_reach(u, v) for u, v in handles))
186
+ timed("depth", len(names), lambda: sum(t.depth(v) for v in names))
187
+
188
+ k = max(1, n // 10)
189
+ t, names, _ = build("random", n)
190
+ victims = rnd.sample(names[1:], k)
191
+ timed("delete", k, lambda: [t.delete(v) for v in victims])
192
+
193
+ t, names, _ = build("random", n)
194
+ moves = []
195
+ while len(moves) < max(1, n // 100):
196
+ u, p = rnd.choice(names[1:]), rnd.choice(names)
197
+ moves.append((u, p))
198
+
199
+ def do_moves():
200
+ for u, p in moves:
201
+ if t.contains(u) and t.contains(p) and u != p and not t.can_reach(u, p):
202
+ t.cut_n_paste(u, p)
203
+
204
+ timed("cut_n_paste", len(moves), do_moves)
205
+
206
+ t, names, _ = build("random", n)
207
+ leaves = [v for v in names if t.is_leaf(v)][: max(1, n // 10)]
208
+ parents = list({t.get_parent(v) for v in leaves if t.get_parent(v)})
209
+ timed("delete_subtree", len(parents), lambda: [t.delete_subtree(v) for v in parents if t.contains(v) and not t.is_root(v)])
210
+
211
+
212
+ def build_with_depth(n, depth, seed=1):
213
+ """A spine (chain) of `depth` nodes; all other nodes hang at random under spine nodes."""
214
+ rnd = random.Random(seed)
215
+ t, names = DTree("D"), ["n0"]
216
+ for i in range(1, n):
217
+ if i < depth:
218
+ parent = names[-1] # extend the spine
219
+ else:
220
+ parent = names[rnd.randrange(min(depth, len(names)))]
221
+ t.insert(parent, f"n{i}")
222
+ names.append(f"n{i}")
223
+ return t, names
224
+
225
+
226
+ def parent_walk(parent, u, v):
227
+ """What classical doubly linked trees do: climb from v towards the root, looking for u."""
228
+ w = parent.get(v)
229
+ while w is not None:
230
+ if w == u:
231
+ return True
232
+ w = parent.get(w)
233
+ return False
234
+
235
+
236
+ def ancestors_in(parent, v, members):
237
+ """Climb once from v towards the root and count the ancestors of v that lie in members."""
238
+ hits, w = 0, parent.get(v)
239
+ while w is not None:
240
+ if w in members:
241
+ hits += 1
242
+ w = parent.get(w)
243
+ return hits
244
+
245
+
246
+ def bench_depth(n, depths, queries):
247
+ print(f"\n### Reachability by depth: DTree vs parent walk ({n:,} nodes, {queries:,} random pairs)\n")
248
+ row("depth", "DTree [µs]", "parent walk [µs]", "factor")
249
+ row("---:", "---:", "---:", "---:")
250
+ for depth in depths:
251
+ t, names = build_with_depth(n, depth)
252
+ parent = t._parent
253
+ rnd = random.Random(2)
254
+ pairs = [(rnd.choice(names), rnd.choice(names)) for _ in range(queries)]
255
+ start = time.perf_counter()
256
+ a = [t.can_reach(u, v) for u, v in pairs]
257
+ d = time.perf_counter() - start
258
+ start = time.perf_counter()
259
+ b = [parent_walk(parent, u, v) for u, v in pairs]
260
+ w = time.perf_counter() - start
261
+ assert a == b
262
+ row(f"{depth:,}", f"{d / queries * 1e6:.2f}", f"{w / queries * 1e6:.2f}", factor(w, d))
263
+
264
+
265
+ def bench_sets(n, depths, k, queries):
266
+ print(f"\n### And/or queries: constructed nodes vs parent walk ({n:,} nodes, set of {k:,} nodes, {queries:,} query nodes)\n")
267
+ row("depth", "question", "DTree [µs]", "parent walk [µs]", "factor")
268
+ row("---:", "---", "---:", "---:", "---:")
269
+ for depth in depths:
270
+ t, names = build_with_depth(n, depth)
271
+ parent = t._parent
272
+ rnd = random.Random(4)
273
+ S = rnd.sample(names, k)
274
+ U = [rnd.choice(names) for _ in range(queries)]
275
+ start = time.perf_counter()
276
+ vmin, vmax, umin, umax = t.vmin(S), t.vmax(S), t.umin(S), t.umax(S)
277
+ build = time.perf_counter() - start
278
+ members = set(S)
279
+ # "reaches": climb from every member looking for u. "reached by": climb once from u,
280
+ # looking each ancestor up in a hash set of the members (the best a parent walk can do).
281
+ for name, v_q, walk_q in (
282
+ ("reaches all", vmin.reaches_all, lambda u: all(parent_walk(parent, u, v) for v in S)),
283
+ ("reaches some", vmax.reaches_some, lambda u: any(parent_walk(parent, u, v) for v in S)),
284
+ ("reached by all", umax.reached_by_all, lambda u: ancestors_in(parent, u, members) == len(members)),
285
+ ("reached by some", umin.reached_by_some, lambda u: ancestors_in(parent, u, members) > 0),
286
+ ):
287
+ start = time.perf_counter()
288
+ a = [v_q(u) for u in U]
289
+ d = time.perf_counter() - start
290
+ start = time.perf_counter()
291
+ b = [walk_q(u) for u in U]
292
+ w = time.perf_counter() - start
293
+ assert a == b, name
294
+ row(f"{depth:,}", name, f"{d / queries * 1e6:.2f}", f"{w / queries * 1e6:.1f}", factor(w, d))
295
+ print(f"\n(Building VMin, VMax, UMin and UMax for {k:,} nodes: {build * 1e3:.1f} ms, once per set.)")
296
+
297
+
298
+ def factor(slow, fast):
299
+ return f"{slow / fast:.1f}×" if slow < 10 * fast else f"{slow / fast:,.0f}×"
300
+
301
+
302
+ def dynamic_workload(n, depth, queries_per_insert, seed=5):
303
+ """The same sequence for every contender: insertions (a spine of `depth` nodes, the
304
+ others attached at random to spine nodes) with random queries among the nodes present."""
305
+ rnd = random.Random(seed)
306
+ steps = []
307
+ for i in range(1, n):
308
+ parent = i - 1 if i < depth else rnd.randrange(min(depth, i))
309
+ qs = [(rnd.randrange(i + 1), rnd.randrange(i + 1)) for _ in range(queries_per_insert)]
310
+ steps.append((parent, i, qs))
311
+ return steps
312
+
313
+
314
+ def run_dtree(steps):
315
+ """DTree with node handles (insert returns the new node's handle)."""
316
+ t, answers = DTree("D"), []
317
+ nodes = [None] * (len(steps) + 1)
318
+ for p, c, qs in steps:
319
+ if c == 1:
320
+ nodes[1] = t.insert("n0", "n1")
321
+ nodes[0] = t.get_node("n0")
322
+ else:
323
+ nodes[c] = t.insert(nodes[p], f"n{c}")
324
+ for u, v in qs:
325
+ answers.append(t.can_reach(nodes[u], nodes[v]))
326
+ return answers
327
+
328
+
329
+ def run_parent_walk(steps):
330
+ parent, answers = {}, []
331
+ for p, c, qs in steps:
332
+ parent[c] = p
333
+ for u, v in qs:
334
+ answers.append(parent_walk(parent, u, v))
335
+ return answers
336
+
337
+
338
+ def run_static_numbers(steps):
339
+ """Pre- and post-order numbers from a traversal, recomputed before a query whenever the
340
+ tree has changed: O(1) queries, but every insertion invalidates all numbers."""
341
+ children, answers = {}, []
342
+ pre = post = None
343
+ for p, c, qs in steps:
344
+ children.setdefault(p, []).append(c)
345
+ pre = None # the insertion invalidates the numbers
346
+ for u, v in qs:
347
+ if pre is None:
348
+ pre, post, clock = {}, {}, 0
349
+ stack = [(0, False)]
350
+ while stack:
351
+ x, done = stack.pop()
352
+ if done:
353
+ post[x] = clock
354
+ clock += 1
355
+ continue
356
+ pre[x] = clock
357
+ clock += 1
358
+ stack.append((x, True))
359
+ stack.extend((k, False) for k in reversed(children.get(x, ())))
360
+ answers.append(pre[u] < pre[v] and post[v] < post[u])
361
+ return answers
362
+
363
+
364
+ def timed(run, steps):
365
+ start = time.perf_counter()
366
+ answers = run(steps)
367
+ return time.perf_counter() - start, answers
368
+
369
+
370
+ def best(run, steps):
371
+ """Best of three runs, or a single run if it takes over two seconds (noise is small then)."""
372
+ sec, answers = timed(run, steps)
373
+ if sec < 2:
374
+ for _ in range(2):
375
+ sec = min(sec, timed(run, steps)[0])
376
+ return sec, answers
377
+
378
+
379
+ def bench_dynamic(n, depths, static_sizes, static_depths=(100, 1_000)):
380
+ timed(run_dtree, dynamic_workload(1_000, 10, 1)) # warm-up
381
+ timed(run_parent_walk, dynamic_workload(1_000, 10, 1))
382
+ print(f"\n### Dynamic updates: insertions and queries alternating ({n:,} nodes)\n")
383
+ print("A step is one insertion followed by 1 or 10 queries between random nodes present at "
384
+ "that moment; µs per step, best of three runs.\n")
385
+ row("depth", "DTree, 1 query", "parent walk, 1 query", "factor",
386
+ "DTree, 10 queries", "parent walk, 10 queries", "factor")
387
+ row("---:", "---:", "---:", "---:", "---:", "---:", "---:")
388
+ for depth in depths:
389
+ cells = [f"{depth:,}"]
390
+ for q in (1, 10):
391
+ steps = dynamic_workload(n, depth, q)
392
+ d, a = best(run_dtree, steps)
393
+ w, b = best(run_parent_walk, steps)
394
+ assert a == b
395
+ cells += [f"{d / len(steps) * 1e6:.2f}", f"{w / len(steps) * 1e6:.2f}", factor(w, d)]
396
+ row(*cells)
397
+ print("\n### Dynamic updates with static pre/post numbers, recomputed after each insertion "
398
+ "(1 query per insertion)\n")
399
+ print("The tree keeps the given depth as it grows: more nodes make it wider, not deeper. "
400
+ "Total time in ms.\n")
401
+ row("depth", "nodes", "DTree [ms]", "parent walk [ms]", "static numbers [ms]")
402
+ row("---:", "---:", "---:", "---:", "---:")
403
+ for depth in static_depths:
404
+ for m in static_sizes:
405
+ steps = dynamic_workload(m, depth, 1)
406
+ d, a = best(run_dtree, steps)
407
+ w, b = best(run_parent_walk, steps)
408
+ r, c = timed(run_static_numbers, steps)
409
+ assert a == b == c
410
+ row(f"{depth:,}", f"{m:,}", f"{d * 1e3:,.1f}", f"{w * 1e3:,.1f}", f"{r * 1e3:,.0f}")
411
+
412
+
413
+ # -- use case: a tree view with expand/collapse --------------------------------------------
414
+
415
+ def tree_view_workload(n, depth, steps, seed=6):
416
+ """Changes to a tree built by build_with_depth(n, depth): each step either inserts a leaf
417
+ under a random spine node or toggles (collapses or expands) a random spine node, and then
418
+ names a random node v whose row is to be shown."""
419
+ rnd = random.Random(seed)
420
+ ops, size = [], n
421
+ for _ in range(steps):
422
+ if rnd.random() < 0.5:
423
+ op = ("insert", f"n{rnd.randrange(depth)}", f"n{size}")
424
+ size += 1
425
+ else:
426
+ op = ("toggle", f"n{rnd.randrange(depth)}", None)
427
+ ops.append(op + (f"n{rnd.randrange(size)}",))
428
+ return ops
429
+
430
+
431
+ def tree_view_dtree(t, ops, rows):
432
+ """DTree: the collapsed nodes are one UMin. The visible row that contains v is
433
+ collapsed.covering(v) (v itself if None); the rows below it follow Q1, jumping over the
434
+ subtree of every collapsed node with subtree_last."""
435
+ collapsed, out = t.umin(), []
436
+ nxt, last = t.next_in_q1, t.subtree_last
437
+ for kind, a, b, v in ops:
438
+ if kind == "insert":
439
+ t.insert(a, b)
440
+ elif a in collapsed:
441
+ collapsed.discard(a)
442
+ else:
443
+ collapsed.add(a)
444
+ x = collapsed.covering(v) or v
445
+ shown = [x]
446
+ while len(shown) < rows:
447
+ if x in collapsed:
448
+ x = last(x)
449
+ x = nxt(x)
450
+ if x is None:
451
+ break
452
+ shown.append(x)
453
+ out.append(shown)
454
+ return out
455
+
456
+
457
+ def tree_view_walk(tree, ops, rows):
458
+ """Classical tree model: parent link, child list and next-sibling link per node, and a set of
459
+ collapsed nodes. The visible row that contains v is its topmost collapsed ancestor (one climb
460
+ to the root); the next row is the first child of an expanded node, otherwise the next sibling
461
+ of the node or of its nearest ancestor that has one."""
462
+ parent, kids, nsib = tree
463
+ collapsed, out = set(), []
464
+ for kind, a, b, v in ops:
465
+ if kind == "insert":
466
+ parent[b] = a
467
+ ks = kids.setdefault(a, [])
468
+ if ks:
469
+ nsib[ks[-1]] = b
470
+ ks.append(b)
471
+ elif a in collapsed:
472
+ collapsed.discard(a)
473
+ else:
474
+ collapsed.add(a)
475
+ top, w = None, parent[v]
476
+ while w is not None:
477
+ if w in collapsed:
478
+ top = w
479
+ w = parent[w]
480
+ x = top or v
481
+ shown = [x]
482
+ while len(shown) < rows:
483
+ ks = kids.get(x)
484
+ if ks and x not in collapsed:
485
+ x = ks[0]
486
+ else:
487
+ while x is not None and x not in nsib:
488
+ x = parent[x]
489
+ if x is None:
490
+ break
491
+ x = nsib[x]
492
+ shown.append(x)
493
+ out.append(shown)
494
+ return out
495
+
496
+
497
+ def classical_copy(t):
498
+ parent, kids, nsib = {}, {}, {}
499
+ for v in t.q1:
500
+ parent[v] = t.get_parent(v)
501
+ ks = t.children(v)
502
+ if ks:
503
+ kids[v] = ks
504
+ nsib.update(zip(ks, ks[1:]))
505
+ return parent, kids, nsib
506
+
507
+
508
+ def bench_tree_view(n, depths, steps, rows=20):
509
+ print(f"\n### Use case: tree view with expand/collapse ({n:,} nodes, {steps:,} steps)\n")
510
+ print("Each step changes the tree (inserts a leaf or collapses/expands a node, half and half), "
511
+ f"then shows a random node: it finds the visible row that contains it and lists the {rows} "
512
+ "visible rows from there. µs per step, best of three runs.\n")
513
+ row("depth", "DTree [µs per step]", "parent walk [µs per step]", "factor")
514
+ row("---:", "---:", "---:", "---:")
515
+ for depth in depths:
516
+ ops = tree_view_workload(n, depth, steps)
517
+ t, _ = build_with_depth(n, depth)
518
+ base = classical_copy(t)
519
+ d = w = float("inf")
520
+ for _ in range(3):
521
+ t, _ = build_with_depth(n, depth)
522
+ sec, a = timed(lambda o: tree_view_dtree(t, o, rows), ops)
523
+ d = min(d, sec)
524
+ tree = ({**base[0]}, {k: list(v) for k, v in base[1].items()}, {**base[2]})
525
+ sec, b = timed(lambda o: tree_view_walk(tree, o, rows), ops)
526
+ w = min(w, sec)
527
+ assert a == b
528
+ row(f"{depth:,}", f"{d / steps * 1e6:.1f}", f"{w / steps * 1e6:.1f}", factor(w, d))
529
+
530
+
531
+ def main():
532
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
533
+ ap.add_argument("sizes", nargs="*", type=int, help="tree sizes (default: 10000 100000)")
534
+ ap.add_argument("--quick", action="store_true", help="small sizes for a fast check")
535
+ ap.add_argument("--dynamic", action="store_true", help="only the dynamic updates and the tree view")
536
+ ap.add_argument("--note", default="", help="remark written into the report (e.g. machine state)")
537
+ ap.add_argument("--no-save", action="store_true", help="do not write a Markdown report")
538
+ args = ap.parse_args()
539
+ started = datetime.datetime.now()
540
+ tee = None
541
+ if not args.no_save:
542
+ tee = _Tee(sys.stdout)
543
+ sys.stdout = tee
544
+ try:
545
+ run(args)
546
+ finally:
547
+ if tee is not None:
548
+ sys.stdout = tee.console
549
+ shown = " ".join(f'"{a}"' if " " in a else a for a in sys.argv[1:])
550
+ path = write_report(tee.copy.getvalue(), started, shown, args.note)
551
+ print(f"\nReport written to {os.path.relpath(path, REPO)}")
552
+
553
+
554
+ def run(args):
555
+ if args.dynamic:
556
+ print(f"DTree benchmarks, Python {sys.version.split()[0]}")
557
+ if args.quick:
558
+ bench_dynamic(5_000, [10, 100, 1_000], [500, 1_000, 2_000])
559
+ bench_tree_view(5_000, [10, 100, 1_000], 1_000)
560
+ else:
561
+ bench_dynamic(20_000, [10, 100, 1_000, 10_000], [1_000, 2_000, 5_000])
562
+ bench_tree_view(20_000, [10, 100, 1_000, 10_000], 5_000)
563
+ return
564
+ sizes = [1_000, 10_000] if args.quick else (args.sizes or [10_000, 100_000])
565
+ print(f"DTree benchmarks, Python {sys.version.split()[0]}")
566
+ bench_build(sizes)
567
+ bench_lists(sizes)
568
+ bench_operations(max(sizes), queries=100_000 if args.quick else 1_000_000)
569
+ if args.quick:
570
+ bench_depth(10_000, [10, 100, 1_000, 10_000], 20_000)
571
+ bench_sets(10_000, [10, 100, 1_000], 100, 200)
572
+ bench_dynamic(5_000, [10, 100, 1_000], [500, 1_000, 2_000])
573
+ bench_tree_view(5_000, [10, 100, 1_000], 1_000)
574
+ else:
575
+ bench_depth(100_000, [10, 100, 1_000, 10_000, 100_000], 20_000)
576
+ bench_sets(100_000, [10, 100, 1_000, 10_000], 1_000, 200)
577
+ bench_dynamic(20_000, [10, 100, 1_000, 10_000], [1_000, 2_000, 5_000])
578
+ bench_tree_view(20_000, [10, 100, 1_000, 10_000], 5_000)
579
+
580
+
581
+ if __name__ == "__main__":
582
+ main()