diff --git a/README.md b/README.md index 0b105b5..c3ccebe 100644 --- a/README.md +++ b/README.md @@ -109,13 +109,13 @@ Crawler detection was also tested against real-world crawler UAs from [monperrus ## Performance -All compared parsers cache repeated User-Agents, making cache hits effectively free. The useful difference is therefore the first parse of a new string. +Startup cost matters for single-item processing, such as a script run per request: from import to the first parsed User-Agent, uarite takes about **10 ms**, while ua-parser takes **50–90 ms** depending on the backend. -![Cold-cache throughput in parses per second: user-agent-parser 122k, uarite 56k, ua-parser Rust 21k, RE2 13k, pure 3k, user-agents 3k](https://git.zi.fi/LeoVasanko/uarite/raw/branch/main/docs/bench-speed.svg) +![Cold-cache throughput in parses per second: user-agent-parser 122k, uarite 56k, ua-parser Rust 21k, RE2 13k, ua-parser 3k, user-agents 3k](https://git.zi.fi/LeoVasanko/uarite/raw/branch/main/docs/bench-speed.svg) -*User-Agents parsed per second, first parse of previously unseen strings (cache cold), equal share of browser and crawler UAs. One-off setup costs excluded — ua-parser's very first parse alone takes ~59 ms loading its regex database.* +_User-Agents parsed per second, first parse of previously unseen strings, equal share of browser and crawler UAs. One-off setup costs excluded._ -In our benchmarks, cold parses of previously unseen UAs (half browsers, half crawlers) take roughly **16 µs** with uarite. user-agent-parser is faster at **8 µs**, while the pure-Python ua-parser/user-agents path takes roughly **320 µs**, which can be a considerable slowdown; ua-parser's native backends help but still trail at ~77 µs (RE2) and ~47 µs (Rust). +All parsers here cache repeated User-Agents, bringing the cost of a repeated string down to nearly zero. The differences only show on the first parse of a new string — exactly the case shown above. ## Design diff --git a/docs/bench-speed.svg b/docs/bench-speed.svg new file mode 100644 index 0000000..42daa8b --- /dev/null +++ b/docs/bench-speed.svg @@ -0,0 +1,712 @@ + + + + + + + + 2026-09-08T21:11:49.096249 + image/svg+xml + + + Matplotlib v3.11.1, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/bench.py b/scripts/bench.py index ac15c8f..2e9ae52 100644 --- a/scripts/bench.py +++ b/scripts/bench.py @@ -34,6 +34,7 @@ from user_agent_parser import parse as uap_parse from user_agents import parse as uas_parse from uarite import uaparse +from uarite.core import _parse_client ALL_DOMAINS = ( ua_parser.Domain.USER_AGENT | ua_parser.Domain.OS | ua_parser.Domain.DEVICE @@ -244,7 +245,7 @@ def bench_realistic(): f" with 2000 mostly-unique bots)" ) for name, fn in ( - ("ua-parser (pure)", uap_pure), + ("ua-parser", uap_pure), ("ua-parser (re2)", uap_re2), ("ua-parser (rust)", uap_rust), ("user-agents", uas_parse), @@ -257,7 +258,7 @@ def bench_realistic(): print(f"{name:20} {t / len(mix) * 1e6:7.1f} µs/UA cache: {info}") print(f"\n## pure bot storm ({len(storm)} unique UAs, zero cache value)") for name, fn in ( - ("ua-parser (pure)", uap_pure), + ("ua-parser", uap_pure), ("ua-parser (re2)", uap_re2), ("ua-parser (rust)", uap_rust), ("user-agents", uas_parse), @@ -283,8 +284,8 @@ def safe(fn): def cache_info(name): if name == "uarite": - i = uaparse.cache_info() - return f"{i.hits} hits / {i.misses} misses (cap 1024)" + i = _parse_client.cache_info() + return f"{i.hits} hits / {i.misses} misses (cap 1024, browsers only)" if name == "user-agent-parser": from user_agent_parser.parser import _cached_parse_user_agent @@ -314,7 +315,7 @@ def bench(): rng.shuffle(work) res = {} for name, fn in ( - ("ua-parser (pure)", uap_pure), + ("ua-parser", uap_pure), ("ua-parser (re2)", uap_re2), ("ua-parser (rust)", uap_rust), ("user-agents", uas_parse), @@ -323,7 +324,7 @@ def bench(): ): fn = safe(fn) fn("Warmup/1.0 (+https://example.com/warmup)") - uaparse.cache_clear() + _parse_client.cache_clear() t = timeit.timeit(lambda: [fn(u) for u in work], number=1) res[name] = t / len(work) * 1e6 return res, len(work) diff --git a/scripts/speedplot.py b/scripts/speedplot.py index 4688ec8..41af822 100644 --- a/scripts/speedplot.py +++ b/scripts/speedplot.py @@ -20,7 +20,7 @@ import matplotlib matplotlib.use("Agg") matplotlib.rcParams["svg.fonttype"] = "path" # text as paths: renders anywhere -import matplotlib.pyplot as plt # noqa: E402 +import matplotlib.pyplot as plt # µs per cold parse, from scripts/bench.py. US = { @@ -28,7 +28,7 @@ US = { "uarite": 17.7, "ua-parser (Rust)": 46.6, "ua-parser (RE2)": 77.1, - "ua-parser (pure)": 321.5, + "ua-parser": 321.5, "user-agents": 334.4, } @@ -42,15 +42,17 @@ colors = ["#6e6e6e"] * len(data) colors[names.index("uarite")] = "#2b6cb0" fig, ax = plt.subplots(figsize=(12, 1.7), dpi=100) -bars = ax.barh(names, values, color=colors, height=0.62) +bars = ax.barh(names, values, color=colors, height=0.82) ax.set_xlim(0, max(values)) ax.axis("off") for bar, name, v in zip(bars, names, values): - # Round to three significant digits: 121951 -> "122 000". - rounded = round(v, 2 - int(f"{v:.0e}".split("e")[1])) + # Round to two significant digits: 121951 -> "120 000". + rounded = round(v, 1 - int(f"{v:.0e}".split("e")[1])) label = f"{name} {rounded:,.0f}".replace(",", " ") - y = bar.get_y() + bar.get_height() / 2 + # va="center" centers the font bbox incl. descender space, which leaves + # the glyphs slightly high; nudge down to optically center on the bar. + y = bar.get_y() + bar.get_height() / 2 - 0.09 if bar.get_width() > max(values) * 0.28: # Long bar: white text inside, right-aligned at the bar end. ax.text(bar.get_width() - max(values) * 0.012, y, label,