ssrjson-benchmark 0.0.3__cp311-cp311-win_amd64.whl → 0.0.4__cp311-cp311-win_amd64.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.

Potentially problematic release.


This version of ssrjson-benchmark might be problematic. Click here for more details.

@@ -0,0 +1,759 @@
1
+ import gc
2
+ import io
3
+ import json
4
+ import math
5
+ import os
6
+ import pathlib
7
+ import platform
8
+ import re
9
+ import sys
10
+ import time
11
+ from importlib.util import find_spec
12
+ from typing import TYPE_CHECKING, Any, Callable, List
13
+
14
+ import matplotlib as mpl
15
+ import matplotlib.pyplot as plt
16
+ import orjson
17
+ import ssrjson
18
+ import ujson
19
+
20
+ from . import _ssrjson_benchmark
21
+ from .result_types import BenchmarkFinalResult, BenchmarkResultPerFile
22
+
23
+ if TYPE_CHECKING:
24
+ from reportlab.pdfgen import canvas
25
+
26
+ mpl.use("Agg")
27
+ mpl.rcParams["svg.fonttype"] = "none"
28
+
29
+
30
+ CUR_FILE = os.path.abspath(__file__)
31
+ CUR_DIR = os.path.dirname(CUR_FILE)
32
+ CWD = os.getcwd()
33
+ _NS_IN_ONE_S = 1000000000
34
+
35
+ PDF_HEADING_FONT = "Helvetica-Bold"
36
+ PDF_TEXT_FONT = "Courier"
37
+
38
+ # baseline is the first one.
39
+ LIBRARIES_COLORS = {
40
+ "json": "#74c476",
41
+ "ujson": "#c994c7",
42
+ "orjson": "#2c7fb8",
43
+ "ssrjson": "#fd8d3c",
44
+ }
45
+
46
+
47
+ class BenchmarkFunction:
48
+ def __init__(self, func: Callable, library_name: str) -> None:
49
+ self.func = func
50
+ self.library_name = library_name
51
+
52
+
53
+ class BenchmarkGroup:
54
+ def __init__(
55
+ self,
56
+ benchmarker: Callable,
57
+ functions: list[BenchmarkFunction],
58
+ group_name: str,
59
+ input_preprocessor: Callable[[Any], Any] = lambda x: x,
60
+ ) -> None:
61
+ self.benchmarker = benchmarker
62
+ self.functions = functions
63
+ self.group_name = group_name
64
+ self.input_preprocessor = input_preprocessor
65
+
66
+
67
+ # benchmarkers
68
+ def _benchmark(repeat_time: int, func, *args):
69
+ """
70
+ Run repeat benchmark, with utf-8 cache.
71
+ returns time used (ns).
72
+ """
73
+ gc_was_enabled = _gc_prepare()
74
+ try:
75
+ # warm up
76
+ _ssrjson_benchmark.run_object_accumulate_benchmark(func, 100, args)
77
+ return _ssrjson_benchmark.run_object_accumulate_benchmark(
78
+ func, repeat_time, args
79
+ )
80
+ finally:
81
+ if gc_was_enabled:
82
+ gc.enable()
83
+
84
+
85
+ def _benchmark_unicode_arg(repeat_time: int, func, unicode: str):
86
+ """
87
+ Run repeat benchmark, disabling utf-8 cache.
88
+ returns time used (ns).
89
+ """
90
+ gc_was_enabled = _gc_prepare()
91
+ try:
92
+ warmup_size = 100
93
+ # prepare identical data, without sharing objects
94
+ warmup_data = _ssrjson_benchmark.copy_unicode_list_invalidate_cache(
95
+ unicode, warmup_size
96
+ )
97
+ benchmark_data = _ssrjson_benchmark.copy_unicode_list_invalidate_cache(
98
+ unicode, repeat_time
99
+ )
100
+ # warm up
101
+ for i in range(warmup_size):
102
+ _ssrjson_benchmark.run_object_benchmark(func, (warmup_data[i],))
103
+ total = 0
104
+ for i in range(repeat_time):
105
+ total += _ssrjson_benchmark.run_object_benchmark(func, (benchmark_data[i],))
106
+ return total
107
+ finally:
108
+ if gc_was_enabled:
109
+ gc.enable()
110
+
111
+
112
+ def _benchmark_invalidate_dump_cache(repeat_time: int, func, raw_bytes: bytes):
113
+ """
114
+ Invalidate utf-8 cache for the same input.
115
+ returns time used (ns).
116
+ """
117
+ # prepare identical data, without sharing objects
118
+ data_warmup = [json.loads(raw_bytes) for _ in range(10)]
119
+ data = [json.loads(raw_bytes) for _ in range(repeat_time)]
120
+ # disable GC
121
+ gc_was_enabled = _gc_prepare()
122
+ try:
123
+ # warm up
124
+ for i in range(10):
125
+ new_args = (data_warmup[i],)
126
+ _ssrjson_benchmark.run_object_benchmark(func, new_args)
127
+ #
128
+ total = 0
129
+ for i in range(repeat_time):
130
+ new_args = (data[i],)
131
+ total += _ssrjson_benchmark.run_object_benchmark(func, new_args)
132
+ return total
133
+ finally:
134
+ if gc_was_enabled:
135
+ gc.enable()
136
+
137
+
138
+ def _get_benchmark_defs() -> tuple[BenchmarkGroup, ...]:
139
+ return (
140
+ BenchmarkGroup(
141
+ _benchmark_invalidate_dump_cache,
142
+ [
143
+ BenchmarkFunction(lambda x: json.dumps(x, ensure_ascii=False), "json"),
144
+ BenchmarkFunction(
145
+ lambda x: ujson.dumps(x, ensure_ascii=False), "ujson"
146
+ ),
147
+ BenchmarkFunction(lambda x: orjson.dumps(x).decode("utf-8"), "orjson"),
148
+ BenchmarkFunction(ssrjson.dumps, "ssrjson"),
149
+ ],
150
+ "dumps",
151
+ ),
152
+ BenchmarkGroup(
153
+ _benchmark_invalidate_dump_cache,
154
+ [
155
+ BenchmarkFunction(
156
+ lambda x: json.dumps(x, indent=2, ensure_ascii=False), "json"
157
+ ),
158
+ BenchmarkFunction(
159
+ lambda x: ujson.dumps(x, indent=2, ensure_ascii=False), "ujson"
160
+ ),
161
+ BenchmarkFunction(
162
+ lambda x: orjson.dumps(x, option=orjson.OPT_INDENT_2).decode(
163
+ "utf-8"
164
+ ),
165
+ "orjson",
166
+ ),
167
+ BenchmarkFunction(lambda x: ssrjson.dumps(x, indent=2), "ssrjson"),
168
+ ],
169
+ "dumps(indented2)",
170
+ ),
171
+ BenchmarkGroup(
172
+ _benchmark_invalidate_dump_cache,
173
+ [
174
+ BenchmarkFunction(
175
+ lambda x: json.dumps(x, ensure_ascii=False).encode("utf-8"), "json"
176
+ ),
177
+ BenchmarkFunction(
178
+ lambda x: ujson.dumps(x, ensure_ascii=False).encode("utf-8"),
179
+ "ujson",
180
+ ),
181
+ BenchmarkFunction(orjson.dumps, "orjson"),
182
+ BenchmarkFunction(ssrjson.dumps_to_bytes, "ssrjson"),
183
+ ],
184
+ "dumps_to_bytes",
185
+ ),
186
+ BenchmarkGroup(
187
+ _benchmark_invalidate_dump_cache,
188
+ [
189
+ BenchmarkFunction(
190
+ lambda x: json.dumps(x, indent=2, ensure_ascii=False).encode(
191
+ "utf-8"
192
+ ),
193
+ "json",
194
+ ),
195
+ BenchmarkFunction(
196
+ lambda x: ujson.dumps(x, indent=2, ensure_ascii=False).encode(
197
+ "utf-8"
198
+ ),
199
+ "ujson",
200
+ ),
201
+ BenchmarkFunction(
202
+ lambda x: orjson.dumps(x, option=orjson.OPT_INDENT_2), "orjson"
203
+ ),
204
+ BenchmarkFunction(
205
+ lambda x: ssrjson.dumps_to_bytes(x, indent=2), "ssrjson"
206
+ ),
207
+ ],
208
+ "dumps_to_bytes(indented2)",
209
+ ),
210
+ BenchmarkGroup(
211
+ _benchmark_unicode_arg,
212
+ [
213
+ BenchmarkFunction(json.loads, "json"),
214
+ BenchmarkFunction(ujson.loads, "ujson"),
215
+ BenchmarkFunction(orjson.loads, "orjson"),
216
+ BenchmarkFunction(ssrjson.loads, "ssrjson"),
217
+ ],
218
+ "loads(str)",
219
+ input_preprocessor=lambda x: x.decode("utf-8"),
220
+ ),
221
+ BenchmarkGroup(
222
+ _benchmark,
223
+ [
224
+ BenchmarkFunction(json.loads, "json"),
225
+ BenchmarkFunction(ujson.loads, "ujson"),
226
+ BenchmarkFunction(orjson.loads, "orjson"),
227
+ BenchmarkFunction(ssrjson.loads, "ssrjson"),
228
+ ],
229
+ "loads(bytes)",
230
+ ),
231
+ )
232
+
233
+
234
+ def _get_benchmark_libraries() -> dict[str, BenchmarkGroup]:
235
+ return {x.group_name: x for x in _get_benchmark_defs()}
236
+
237
+
238
+ def _gc_prepare():
239
+ """
240
+ Call collect once, and then disable automatic GC.
241
+ Return True if automatic GC was enabled.
242
+ """
243
+ gc.collect()
244
+ gc_was_enabled = gc.isenabled()
245
+ if gc_was_enabled:
246
+ gc.disable()
247
+ return gc_was_enabled
248
+
249
+
250
+ def _get_processed_size(func: Callable, input_data, is_dumps):
251
+ if is_dumps:
252
+ # get output size of dumps
253
+ data_obj = json.loads(input_data)
254
+ output = func(data_obj)
255
+ if isinstance(output, bytes):
256
+ size = len(output)
257
+ else:
258
+ size = _ssrjson_benchmark.inspect_pyunicode(output)[1]
259
+ else:
260
+ # get loads input size
261
+ size = (
262
+ len(input_data)
263
+ if isinstance(input_data, bytes)
264
+ else _ssrjson_benchmark.inspect_pyunicode(input_data)[1]
265
+ )
266
+ return size
267
+
268
+
269
+ def _run_benchmark(
270
+ cur_result_file: BenchmarkResultPerFile,
271
+ repeat_times: int,
272
+ input_data: str | bytes,
273
+ benchmark_group: BenchmarkGroup,
274
+ ):
275
+ group_name = benchmark_group.group_name
276
+ cur_target = cur_result_file[group_name]
277
+
278
+ input_data = benchmark_group.input_preprocessor(input_data)
279
+
280
+ for benchmark_target in benchmark_group.functions:
281
+ speed = benchmark_group.benchmarker(
282
+ repeat_times, benchmark_target.func, input_data
283
+ )
284
+ cur_lib = cur_target[benchmark_target.library_name]
285
+ cur_lib.speed = speed
286
+
287
+ baseline_name = "json"
288
+ baseline_data = cur_target[baseline_name]
289
+ for benchmark_target in benchmark_group.functions:
290
+ cur_lib = cur_target[benchmark_target.library_name]
291
+ if benchmark_target.library_name == "ssrjson":
292
+ # calculate bytes per sec for ssrJSON
293
+ size = _get_processed_size(
294
+ benchmark_target.func, input_data, "dumps" in group_name
295
+ )
296
+ cur_target.ssrjson_bytes_per_sec = (
297
+ size * repeat_times / (cur_lib.speed / _NS_IN_ONE_S)
298
+ )
299
+
300
+ cur_lib.ratio = (
301
+ math.inf
302
+ if baseline_data.speed == 0
303
+ else (baseline_data.speed / cur_lib.speed)
304
+ )
305
+
306
+
307
+ def _run_file_benchmark(
308
+ benchmark_libraries: dict[str, BenchmarkGroup],
309
+ file: pathlib.Path,
310
+ process_bytes: int,
311
+ ):
312
+ print(f"Running benchmark for {file.name}")
313
+ with open(file, "rb") as f:
314
+ raw_bytes = f.read()
315
+ raw = raw_bytes.decode("utf-8")
316
+ base_file_name = os.path.basename(file)
317
+ cur_result_file = BenchmarkResultPerFile()
318
+ cur_result_file.byte_size = bytes_size = len(raw_bytes)
319
+ kind, str_size, is_ascii, _ = _ssrjson_benchmark.inspect_pyunicode(raw)
320
+ cur_result_file.pyunicode_size = str_size
321
+ cur_result_file.pyunicode_kind = kind
322
+ cur_result_file.pyunicode_is_ascii = is_ascii
323
+ repeat_times = int((process_bytes + bytes_size - 1) // bytes_size)
324
+
325
+ for benchmark_group in benchmark_libraries.values():
326
+ _run_benchmark(cur_result_file, repeat_times, raw_bytes, benchmark_group)
327
+ return base_file_name, cur_result_file
328
+
329
+
330
+ def _get_head_rev_name():
331
+ return (
332
+ getattr(ssrjson, "__version__", None) or getattr(ssrjson, "ssrjson").__version__
333
+ )
334
+
335
+
336
+ def _get_real_output_file_name():
337
+ rev = _get_head_rev_name()
338
+ if not rev:
339
+ file = "benchmark_result.json"
340
+ else:
341
+ file = f"benchmark_result_{rev}.json"
342
+ return file
343
+
344
+
345
+ def _get_cpu_name() -> str:
346
+ cpuinfo_spec = find_spec("cpuinfo")
347
+ if cpuinfo_spec is not None:
348
+ import cpuinfo
349
+
350
+ cpu_name = cpuinfo.get_cpu_info().get("brand_raw", "UnknownCPU")
351
+ else:
352
+ # fallback
353
+ cpu_name: str = platform.processor()
354
+ if cpu_name.strip() == "":
355
+ # linux fallback
356
+ if os.path.exists("/proc/cpuinfo"):
357
+ with open(file="/proc/cpuinfo", mode="r") as file:
358
+ cpu_info_lines = file.readlines()
359
+ for line in cpu_info_lines:
360
+ if "model name" in line:
361
+ cpu_name = re.sub(
362
+ pattern=r"model name\s+:\s+", repl="", string=line
363
+ )
364
+ break
365
+ else:
366
+ cpu_name = "UnknownCPU"
367
+ # merge nearby spaces
368
+ return re.sub(pattern=r"\s+", repl=" ", string=cpu_name).strip()
369
+
370
+
371
+ def _get_mem_total() -> str:
372
+ mem_total: int = 0
373
+ if platform.system() == "Linux":
374
+ with open(file="/proc/meminfo", mode="r") as file:
375
+ mem_info_lines = file.readlines()
376
+ for line in mem_info_lines:
377
+ if "MemTotal" in line:
378
+ mem_total = int(re.sub(pattern=r"[^0-9]", repl="", string=line))
379
+ break
380
+ elif platform.system() == "Windows":
381
+ import psutil
382
+
383
+ mem_total = psutil.virtual_memory().total // (1024 * 1024)
384
+ return f"{mem_total / (1024**2):.3f}GiB"
385
+
386
+
387
+ def _get_ratio_color(ratio: float) -> str:
388
+ if ratio < 1:
389
+ return "#d63031" # red (worse than baseline)
390
+ elif ratio == 1:
391
+ return "black" # black (baseline)
392
+ elif ratio < 2:
393
+ return "#e67e22" # orange (similar/slightly better)
394
+ elif ratio < 4:
395
+ return "#f39c12" # amber (decent improvement)
396
+ elif ratio < 8:
397
+ return "#27ae60" # green (good)
398
+ elif ratio < 16:
399
+ return "#2980b9" # blue (great)
400
+ else:
401
+ return "#8e44ad" # purple (exceptional)
402
+
403
+
404
+ def _plot_relative_ops(
405
+ catagories: list[str], data: dict, doc_name: str, index_s: str
406
+ ) -> io.BytesIO:
407
+ libs = list(LIBRARIES_COLORS.keys())
408
+ colors = [LIBRARIES_COLORS[n] for n in libs]
409
+ n = len(catagories)
410
+ bar_width = 0.2
411
+ inner_pad = 0
412
+
413
+ fig, axs = plt.subplots(
414
+ 1,
415
+ n,
416
+ figsize=(4 * n, 6),
417
+ sharey=False,
418
+ tight_layout=True,
419
+ gridspec_kw={"wspace": 0},
420
+ )
421
+
422
+ x_positions = [i * (bar_width + inner_pad) for i in range(len(libs))]
423
+
424
+ for ax, cat in zip(axs, catagories):
425
+ vals = [1.0] + [data[cat][name]["ratio"] for name in libs[1:]]
426
+ gbps = (data[cat]["ssrjson_bytes_per_sec"]) / (1024**3)
427
+
428
+ for xi, val, col in zip(x_positions, vals, colors):
429
+ ax.bar(xi, val, width=bar_width, color=col)
430
+ ax.text(
431
+ xi,
432
+ val + 0.05,
433
+ f"{val:.2f}x",
434
+ ha="center",
435
+ va="bottom",
436
+ fontsize=9,
437
+ color=_get_ratio_color(val),
438
+ )
439
+
440
+ ssrjson_index = libs.index("ssrjson")
441
+ ax.text(
442
+ x_positions[ssrjson_index],
443
+ vals[ssrjson_index] / 2,
444
+ f"{gbps:.2f} GB/s",
445
+ ha="center",
446
+ va="center",
447
+ fontsize=10,
448
+ color="#2c3e50",
449
+ fontweight="bold",
450
+ )
451
+
452
+ # baseline line
453
+ ax.axhline(1.0, color="gray", linestyle="--", linewidth=1)
454
+ # height = 1.1 * max bar height
455
+ ax.set_ylim(0, max(vals + [1.0]) * 1.1)
456
+
457
+ # hide all tick
458
+ ax.tick_params(
459
+ axis="both",
460
+ which="both",
461
+ left=False,
462
+ bottom=False,
463
+ labelleft=False,
464
+ labelbottom=False,
465
+ )
466
+
467
+ # and spine
468
+ for spine in ("left", "top", "right"):
469
+ ax.spines[spine].set_visible(False)
470
+
471
+ ax.set_xlabel(cat, fontsize=10, labelpad=6)
472
+
473
+ fig.suptitle(
474
+ doc_name,
475
+ fontsize=20,
476
+ fontweight="bold",
477
+ y=0.98,
478
+ )
479
+
480
+ # color legend
481
+ legend_elements = [
482
+ plt.Line2D([0], [0], color=col, lw=4, label=name)
483
+ for name, col in LIBRARIES_COLORS.items()
484
+ ]
485
+ fig.legend(
486
+ handles=legend_elements,
487
+ loc="upper right",
488
+ bbox_to_anchor=(0.98, 0.95),
489
+ ncol=len(libs),
490
+ fontsize=14,
491
+ frameon=False,
492
+ )
493
+
494
+ fig.text(
495
+ 0.5,
496
+ 0,
497
+ "Higher is better",
498
+ ha="center",
499
+ va="bottom",
500
+ fontsize=8,
501
+ style="italic",
502
+ color="#555555",
503
+ )
504
+
505
+ buf = io.BytesIO()
506
+ plt.savefig(buf, format="svg", bbox_inches="tight")
507
+ buf.seek(0)
508
+ plt.close(fig)
509
+ return buf
510
+
511
+
512
+ def _draw_page_number(c: "canvas.Canvas", page_num: int):
513
+ from reportlab.lib.pagesizes import A4
514
+
515
+ width, _ = A4
516
+ c.setFont("Helvetica-Oblique", 8) # italic
517
+ c.setFillColorRGB(0.5, 0.5, 0.5) # grey
518
+ c.drawRightString(width - 40, 20, f"{page_num}")
519
+
520
+
521
+ def _generate_pdf_report(
522
+ figures: List[List[io.BytesIO]], header_text: str, output_pdf_path: str
523
+ ) -> str:
524
+ from reportlab.graphics import renderPDF
525
+ from reportlab.lib.pagesizes import A4
526
+ from reportlab.pdfgen import canvas
527
+ from svglib.svglib import svg2rlg
528
+
529
+ try:
530
+ from svglib.fonts import FontMap
531
+
532
+ font_map = FontMap()
533
+ font_map.register_default_fonts()
534
+ # workaround for matplotlib using 700 to represent bold font, but svg2rlg using 700 as normal.
535
+ font_map.register_font("Helvetica", weight="700", rlgFontName="Helvetica-Bold")
536
+ except ImportError:
537
+ font_map = None
538
+
539
+ c = canvas.Canvas(output_pdf_path, pagesize=A4)
540
+ width, height = A4
541
+
542
+ # heading info
543
+ heading = header_text.splitlines()
544
+ # first line is # header
545
+ header, heading_info = heading[0].removeprefix("#").strip(), heading[1:]
546
+ c.setFont(PDF_HEADING_FONT, 20)
547
+ text_obj = c.beginText(40, height - 50)
548
+ text_obj.textLine(header)
549
+ c.drawText(text_obj)
550
+
551
+ # Wrap heading_info lines if overflow
552
+ max_width = width - 80 # 40 margin on both sides
553
+ wrapped_heading_info = []
554
+ for line in heading_info:
555
+ while c.stringWidth(line, PDF_TEXT_FONT, 10) > max_width:
556
+ # Find a split point
557
+ split_idx = int(max_width // c.stringWidth(" ", PDF_TEXT_FONT, 10))
558
+ # Try to split at nearest space before split_idx
559
+ space_idx = line.rfind(" ", 0, split_idx)
560
+ if space_idx == -1:
561
+ space_idx = split_idx
562
+ wrapped_heading_info.append(line[:space_idx])
563
+ # TODO fixed indent
564
+ line = " " + line[space_idx:].lstrip()
565
+ wrapped_heading_info.append(line)
566
+ heading_info = wrapped_heading_info
567
+
568
+ c.setFont(PDF_TEXT_FONT, 10)
569
+ text_obj = c.beginText(40, height - 70)
570
+ for line in heading_info:
571
+ text_obj.textLine(line)
572
+ c.drawText(text_obj)
573
+
574
+ c.setFont("Helvetica-Oblique", 8)
575
+ text = "This report was generated by https://github.com/Nambers/ssrJSON-benchmark"
576
+ c.drawString(40, 20, text)
577
+ link_start = 40 + c.stringWidth("This report was generated by ")
578
+ link_end = link_start + c.stringWidth(
579
+ "https://github.com/Nambers/ssrJSON-benchmark"
580
+ )
581
+ text_height = 5 # Adjusted height to better fit the link area
582
+ c.linkURL(
583
+ "https://github.com/Nambers/ssrJSON-benchmark",
584
+ (link_start, 20, link_end, 20 + text_height),
585
+ relative=1,
586
+ )
587
+
588
+ header_lines = header_text.count("\n") + 1
589
+ header_height = header_lines * 14 + 10
590
+ # subheading spacing = 30
591
+ y_pos = height - header_height - 30
592
+ bottom_margin = 20
593
+ vertical_gap = 20
594
+
595
+ p = 0
596
+
597
+ for name, figs in zip(["speed"], figures):
598
+ text_obj = c.beginText()
599
+ text_obj.setTextOrigin(40, y_pos)
600
+ text_obj.setFont(PDF_HEADING_FONT, 14)
601
+ text_obj.textLine(f"{name}")
602
+ c.drawText(text_obj)
603
+ c.bookmarkHorizontal(name, 0, y_pos + 20)
604
+ c.addOutlineEntry(name, name, level=0)
605
+ y_pos -= 20
606
+ for svg_io in figs:
607
+ svg_io.seek(0)
608
+ drawing = svg2rlg(svg_io, font_map=font_map)
609
+
610
+ avail_w = width - 80
611
+ scale = avail_w / drawing.width
612
+ drawing.width *= scale
613
+ drawing.height *= scale
614
+ drawing.scale(scale, scale)
615
+
616
+ img_h = drawing.height
617
+ # no enough space
618
+ if y_pos - img_h - vertical_gap < bottom_margin:
619
+ _draw_page_number(c, p)
620
+ p += 1
621
+ c.showPage()
622
+ y_pos = height - bottom_margin
623
+
624
+ c.setStrokeColorRGB(0.9, 0.9, 0.9)
625
+ c.setLineWidth(0.4)
626
+ c.line(40, y_pos, width - 40, y_pos)
627
+
628
+ renderPDF.draw(drawing, c, 40, y_pos - img_h)
629
+ y_pos -= img_h + vertical_gap
630
+
631
+ _draw_page_number(c, p)
632
+ c.save()
633
+ return output_pdf_path
634
+
635
+
636
+ def _fetch_header(rev) -> str:
637
+ with open(os.path.join(CUR_DIR, "template.md"), "r") as f:
638
+ template = f.read()
639
+ return template.format(
640
+ REV=rev,
641
+ TIME=time.strftime("%Y-%m-%d %H:%M:%S %Z", time.localtime()),
642
+ OS=f"{platform.system()} {platform.machine()} {platform.release()} {platform.version()}",
643
+ PYTHON=sys.version,
644
+ ORJSON_VER=orjson.__version__,
645
+ UJSON_VER=ujson.__version__,
646
+ SIMD_FLAGS=ssrjson.get_current_features(),
647
+ CHIPSET=_get_cpu_name(),
648
+ MEM=_get_mem_total(),
649
+ )
650
+
651
+
652
+ def generate_report_pdf(result: BenchmarkFinalResult, file: str, out_dir: str = CWD):
653
+ """
654
+ Generate PDF report, using `result`.
655
+ """
656
+ catagories = result.catagories
657
+ file = file.removesuffix(".json")
658
+ report_name = f"{file}.pdf"
659
+
660
+ figures = []
661
+
662
+ index_s = "speed"
663
+ tmp = []
664
+ for bench_filename in result.results:
665
+ print(f"Processing {bench_filename} (PDF)")
666
+ tmp.append(
667
+ _plot_relative_ops(
668
+ catagories,
669
+ result.results[bench_filename],
670
+ bench_filename,
671
+ index_s,
672
+ )
673
+ )
674
+ figures.append(tmp)
675
+
676
+ template = _fetch_header(
677
+ file.removeprefix("benchmark_result_").removesuffix(".json")
678
+ )
679
+ out_path = _generate_pdf_report(
680
+ figures,
681
+ header_text=template,
682
+ output_pdf_path=os.path.join(out_dir, report_name),
683
+ )
684
+ print(f"Report saved to {out_path}")
685
+ return out_path
686
+
687
+
688
+ def generate_report_markdown(
689
+ result: BenchmarkFinalResult, file: str, out_dir: str = CWD
690
+ ):
691
+ """
692
+ Generate Markdown report, using `result`.
693
+ """
694
+ file = file.removesuffix(".json")
695
+ report_name = f"{file}.md"
696
+ report_folder = os.path.join(out_dir, f"{file}_report")
697
+
698
+ # mkdir
699
+ if not os.path.exists(report_folder):
700
+ os.makedirs(report_folder)
701
+
702
+ template = _fetch_header(
703
+ file.removeprefix("benchmark_result_").removesuffix(".json")
704
+ )
705
+
706
+ index_s = "speed"
707
+ template += f"\n\n## {index_s}\n\n"
708
+ for bench_filename in result.results:
709
+ print(f"Processing {bench_filename} (Markdown)")
710
+ with open(
711
+ os.path.join(report_folder, bench_filename + ".svg"), "wb"
712
+ ) as svg_file:
713
+ svg_file.write(
714
+ _plot_relative_ops(
715
+ result.catagories,
716
+ result.results[bench_filename],
717
+ bench_filename,
718
+ index_s,
719
+ ).getvalue()
720
+ )
721
+ # add svg
722
+ template += f"![{bench_filename}](./{bench_filename}.svg)\n\n"
723
+
724
+ ret = os.path.join(report_folder, report_name)
725
+ with open(ret, "w") as f:
726
+ f.write(template)
727
+ print(f"Report saved to {ret}")
728
+ return ret
729
+
730
+
731
+ def parse_file_result(j):
732
+ return BenchmarkFinalResult.parse(j)
733
+
734
+
735
+ def run_benchmark(files: list[pathlib.Path], process_bytes: int = int(1e8)):
736
+ """
737
+ Generate a JSON result of benchmark.
738
+ Also returns a result object.
739
+ """
740
+ file = _get_real_output_file_name()
741
+
742
+ result = BenchmarkFinalResult()
743
+ result.results = dict()
744
+
745
+ benchmark_libraries = _get_benchmark_libraries()
746
+
747
+ result.catagories = sorted(list(benchmark_libraries.keys()))
748
+
749
+ for bench_file in files:
750
+ k, v = _run_file_benchmark(benchmark_libraries, bench_file, process_bytes)
751
+ result.results[k] = v
752
+ output_result = result.dumps()
753
+
754
+ if os.path.exists(file):
755
+ os.remove(file)
756
+
757
+ with open(f"{file}", "w", encoding="utf-8") as f:
758
+ f.write(output_result)
759
+ return result, file