diff --git a/AGENTS.md b/AGENTS.md index 227ca4f..01a75c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,15 +25,18 @@ Keep benchmark dependencies here. Do not modify Ordered as part of a benchmark c - Write in simple, plain English. Use short sentences and everyday words. - Use Oxford commas in inline lists: "a, b, and c" not "a, b, c". - Do not use em dashes. Restructure the sentence, or use a semicolon instead. Use emojis scarcely. -- Avoid colons in the middle of sentences. A colon that introduces a code block or a list is fine; existing text is reworded when it is touched anyway, not in a sweep. +- Avoid colons in the middle of sentences. A colon that introduces a code block or a list is fine; existing text is reworded when it is touched + anyway, not in a sweep. - Avoid colorful adjectives and adverbs. Write "adjacency query" not "blazing adjacency query". - Prefer noun phrases for checklist items over imperative verbs. Write "temp directory teardown" not "tear down the temp directory". - Headings in Markdown files must be in title case: "Build from Source" not "Build from source". Minor words (a, an, the, and, but, or, for, in, on, at, to, by, of, is, are, was, were, be) stay lowercase unless they are the first word. - Do not bold the lead-in of a list item. Write "Vector and set similarity: ..." not "Vector and set similarity: ...". - Use sentence case for the lead-in of a list item. Write "Seed selection: ..." not "Seed Selection: ...". Proper nouns keep their capitals. -- Capitalize only the first part of a hyphenated compound: "Full-text Search" in a heading, "Breadth-first" at the start of a sentence, and "breadth-first search" elsewhere. Never write "Breadth-First". -- Start each sentence with a capital letter, capitalize proper nouns (Rust, PostgreSQL, SvelteKit, TypeScript), and leave common nouns lowercase in the middle of a sentence. +- Capitalize only the first part of a hyphenated compound: "Full-text Search" in a heading, "Breadth-first" at the start of a sentence, and + "breadth-first search" elsewhere. Never write "Breadth-First". +- Start each sentence with a capital letter, capitalize proper nouns (Rust, PostgreSQL, SvelteKit, TypeScript), and leave common nouns lowercase in + the middle of a sentence. - Write correct and complete sentences. - Avoid made-up words and abbreviations. - Use participial phrases scarcely. diff --git a/build.zig.zon b/build.zig.zon index a46869e..637818f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,8 +5,8 @@ .minimum_zig_version = "0.17.0", .dependencies = .{ .ordered = .{ - .url = "git+https://github.com/CogitatorTech/ordered.git#864d82da5cc060e668cb4fb9c0de5450fcf47985", - .hash = "ordered-0.3.1-Gy41sESfAgBfT7YmlunnfbKOdhdfC0SvZDDylfmjIbp2", + .url = "git+https://github.com/CogitatorTech/ordered.git#5f4642563b9d0624ec38500e43a110ce0bdde173", + .hash = "ordered-0.3.2-Gy41sCOlAgAT_AYvuIjHEA8B1AiyIvaVyDXR4zbEah44", .lazy = true, }, }, diff --git a/flake.lock b/flake.lock index 87694c4..475de09 100644 --- a/flake.lock +++ b/flake.lock @@ -67,17 +67,17 @@ "ordered": { "flake": false, "locked": { - "lastModified": 1792959608, - "narHash": "sha256-Mifmp/llpvX9wfT0nk5vPBvL5GrASmY0VJjsz/o6uzc=", + "lastModified": 1793899826, + "narHash": "sha256-pszTfMvEQFmUS+XDfuGn7m0DhIma02ltqiSz9bDo4dQ=", "owner": "CogitatorTech", "repo": "ordered", - "rev": "864d82da5cc060e668cb4fb9c0de5450fcf47985", + "rev": "5f4642563b9d0624ec38500e43a110ce0bdde173", "type": "github" }, "original": { "owner": "CogitatorTech", "repo": "ordered", - "rev": "864d82da5cc060e668cb4fb9c0de5450fcf47985", + "rev": "5f4642563b9d0624ec38500e43a110ce0bdde173", "type": "github" } }, diff --git a/flake.nix b/flake.nix index 10271d3..cf06b44 100644 --- a/flake.nix +++ b/flake.nix @@ -8,7 +8,7 @@ rust-overlay.url = "github:oxalica/rust-overlay"; rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; ordered = { - url = "github:CogitatorTech/ordered/864d82da5cc060e668cb4fb9c0de5450fcf47985"; + url = "github:CogitatorTech/ordered/5f4642563b9d0624ec38500e43a110ce0bdde173"; flake = false; }; klib = { diff --git a/scripts/report.py b/scripts/report.py index 6e52515..e638942 100644 --- a/scripts/report.py +++ b/scripts/report.py @@ -50,6 +50,17 @@ PHASES = ("insert", "hit", "miss", "iterate", "remove") DOWNLOADS = ("summary.csv", "samples.csv", "metadata.json", "flake.lock") +LANGUAGE_NAMES = { + "c": "C", + "cpp": "C++", + "java": "Java", + "rust": "Rust", + "zig": "Zig", +} + + +def format_language(lang): + return LANGUAGE_NAMES.get(lang, lang.capitalize() if lang else "") def find_latest_results(base_dir=None): @@ -251,7 +262,7 @@ def render_environment(meta): versions = meta.get("versions", {}) if versions: version_summary = ", ".join( - f"{lang}: {ver.splitlines()[0]}" for lang, ver in versions.items() + f"{format_language(lang)}: {ver.splitlines()[0]}" for lang, ver in versions.items() ) lines.append(f"| Toolchain Versions | {version_summary} |") @@ -272,7 +283,7 @@ def render_interpretation_guide(): - **Iterate**: Time to perform an in-order scan across all elements. Highlights cache line density and memory locality (for example, flat arrays and B-tree leaf nodes versus pointer-chasing binary trees). - **Remove**: Time to remove all $N$ keys until the container is empty. Native runners include node deallocation time. Java removal unlinks entries and does not force garbage collection. -Bold values mark the lowest measured median, not a proven winner. Cells show sample IQR and fork medians when raw data is available. These describe spread, not confidence intervals. Samples within a fork are not independent process runs. +Bold values mark the lowest measured median, not a proven winner. ### Architectural Comparisons @@ -387,10 +398,6 @@ def generate_report(records, metadata): key = (r["language"], r["implementation"]) by_impl.setdefault(key, {})[r["phase"]] = r["median_ns_per_op"] - details = { - (r["language"], r["implementation"], r["phase"]): "
" + html.escape( - spread_text(r)) + "" - for r in matching if "q1" in r or r.get("summary_only")} sorted_impls = sorted(by_impl.keys(), key=lambda k: (k[0], k[1])) best_by_phase = {} @@ -424,23 +431,18 @@ def generate_report(records, metadata): short_label = mapping[2] if len(mapping) > 2 else friendly_name insert_str = format_metric(phases.get("insert"), best_by_phase.get("insert"), - runners_up_by_phase.get("insert")) + details.get( - (lang, impl, "insert"), "") + runners_up_by_phase.get("insert")) hit_str = format_metric(phases.get("hit"), best_by_phase.get("hit"), - runners_up_by_phase.get("hit")) + details.get( - (lang, impl, "hit"), "") + runners_up_by_phase.get("hit")) miss_str = format_metric(phases.get("miss"), best_by_phase.get("miss"), - runners_up_by_phase.get("miss")) + details.get( - (lang, impl, "miss"), "") + runners_up_by_phase.get("miss")) iter_str = format_metric(phases.get("iterate"), best_by_phase.get("iterate"), - runners_up_by_phase.get("iterate")) + details.get( - (lang, impl, "iterate"), "") + runners_up_by_phase.get("iterate")) remove_str = format_metric(phases.get("remove"), best_by_phase.get("remove"), - runners_up_by_phase.get("remove")) + details.get( - (lang, impl, "remove"), "") + runners_up_by_phase.get("remove")) table_rows.append( - f"| {lang} | {friendly_name} | {structure} | {insert_str} | {hit_str} | {miss_str} | {iter_str} | {remove_str} |" + f"| {format_language(lang)} | {friendly_name} | {structure} | {insert_str} | {hit_str} | {miss_str} | {iter_str} | {remove_str} |" ) chart_labels.append(short_label) @@ -454,9 +456,6 @@ def generate_report(records, metadata): chart = render_chart(title, chart_labels, values) lines.append(chart) - lines.extend(["## Latency by Dataset Size", "", - "The x-axis shows element counts with equal spacing between measured sizes.", "", - scaling_charts(records)]) return "\n".join(lines) @@ -471,7 +470,7 @@ def scaling_charts(records, as_html=False): rows.sort(key=lambda r: r["size"]) if len(rows) < 2: continue - title = f"{language} {impl} {phase.title()} Scaling ({order}, Seed {seed})" + title = f"{format_language(language)} {impl} {phase.title()} Scaling ({order}, Seed {seed})" code = render_chart_code(title, [str(r["size"]) for r in rows], [r["median_ns_per_op"] for r in rows]).replace(" bar [", " line [") @@ -509,7 +508,7 @@ def render_html_environment(meta): versions = meta.get("versions", {}) if versions: version_summary = ", ".join( - f"{lang}: {ver.splitlines()[0]}" for lang, ver in versions.items() + f"{format_language(lang)}: {ver.splitlines()[0]}" for lang, ver in versions.items() ) rows.append(("Toolchain Versions", html.escape(version_summary))) @@ -539,19 +538,19 @@ def render_html_interpretation_guide():

Performance Interpretation Guide

Ascending and shuffled describe insertion order only. Lookup and removal use independent shuffled permutations. Containers are populated and validated before read and removal phases, so these measure recently accessed data. CI timings are exploratory; publish comparisons from a dedicated idle machine with recorded CPU affinity. This benchmark isolates five phases of sorted container operations:

-

Bold green values mark the lowest measured median, and light orange values mark the next two lowest medians, not proven winners. Cells show sample IQR, sample counts, fork counts, and fork medians. These describe spread, not confidence intervals. Samples within a fork are not independent process runs.

+

Bold green values mark the lowest measured median, and light orange values mark the next two lowest medians, not proven winners.

Key Architectural Comparisons

    -
  1. B-trees versus binary trees: B-trees (Ordered BTreeMap, Rust BTreeMap, Abseil btree_map, and Klib kbtree) pack multiple keys per node. This can reduce pointer chasing compared to node-allocated trees (std::map, Java TreeMap, and Zig RedBlackTreeSet).
  2. -
  3. Intrusive and dedicated AVL trees: Klib kavl and fastutil AVLTreeMap use strict AVL height balancing. Their balance rules differ from red-black trees. Measured latency also depends on node layout, allocation, and input.
  4. -
  5. Flat arrays: Boost flat_map and flat_set store elements in contiguous sorted memory. The layout can benefit reads, but random insertions and removals require shifting elements (O(N) per operation), making large random mutation workloads expensive.
  6. -
  7. Java boxed collections versus primitive specialization: Java's standard TreeMap<Long, Long> stores boxed object pointers, incurring substantial GC and pointer overhead. The fastutil collections operate directly on primitive 64-bit integers (long), avoiding boxed key and value storage. Boxed inputs are prepared outside timers, so boxing allocation cost is excluded.
  8. +
  9. B-trees versus binary trees: B-trees (Ordered BTreeMap, Rust BTreeMap, Abseil btree_map, and Klib kbtree) pack multiple keys per node. This can reduce pointer chasing compared to node-allocated trees (std::map, Java TreeMap, and Zig RedBlackTreeSet).
  10. +
  11. Intrusive and dedicated AVL trees: Klib kavl and fastutil AVLTreeMap use strict AVL height balancing. Their balance rules differ from red-black trees. Measured latency also depends on node layout, allocation, and input.
  12. +
  13. Flat arrays: Boost flat_map and flat_set store elements in contiguous sorted memory. The layout can benefit reads, but random insertions and removals require shifting elements (O(N) per operation), making large random mutation workloads expensive.
  14. +
  15. Java boxed collections versus primitive specialization: Java's standard TreeMap<Long, Long> stores boxed object pointers, incurring substantial GC and pointer overhead. The fastutil collections operate directly on primitive 64-bit integers (long), avoiding boxed key and value storage. Boxed inputs are prepared outside timers, so boxing allocation cost is excluded.
""" @@ -663,7 +662,6 @@ def generate_html_report(records, metadata, downloads=()): " th { background: var(--table-header); font-weight: 600; }", " tr:nth-child(even) td { background: var(--table-stripe); }", " tr:last-child td { border-bottom: none; }", - " td small { display: block; min-width: 11rem; color: var(--text-muted); font-size: 0.7rem; }", " [hidden] { display: none !important; }", " td.num, th.num {", " text-align: right;", @@ -724,6 +722,9 @@ def generate_html_report(records, metadata, downloads=()): " padding: 0.15rem 0.35rem;", " border-radius: 4px;", " }", + " strong.runner-up {", + " font-weight: 700;", + " }", " .chart-card {", " background: var(--card-bg);", " border: 1px solid var(--border);", @@ -806,10 +807,6 @@ def generate_html_report(records, metadata, downloads=()): key = (r["language"], r["implementation"]) by_impl.setdefault(key, {})[r["phase"]] = r["median_ns_per_op"] - details = { - (r["language"], r["implementation"], r["phase"]): "
" + html.escape( - spread_text(r)) + "" - for r in matching if "q1" in r or r.get("summary_only")} sorted_impls = sorted(by_impl.keys(), key=lambda k: (k[0], k[1])) best_by_phase = {} @@ -855,15 +852,15 @@ def generate_html_report(records, metadata, downloads=()): structure = mapping[1] short_label = mapping[2] if len(mapping) > 2 else friendly_name - insert_str = format_html_metric(phases.get("insert"), best_by_phase.get("insert"), runners_up_by_phase.get("insert")) + details.get((lang, impl, "insert"), "") - hit_str = format_html_metric(phases.get("hit"), best_by_phase.get("hit"), runners_up_by_phase.get("hit")) + details.get((lang, impl, "hit"), "") - miss_str = format_html_metric(phases.get("miss"), best_by_phase.get("miss"), runners_up_by_phase.get("miss")) + details.get((lang, impl, "miss"), "") - iter_str = format_html_metric(phases.get("iterate"), best_by_phase.get("iterate"), runners_up_by_phase.get("iterate")) + details.get((lang, impl, "iterate"), "") - remove_str = format_html_metric(phases.get("remove"), best_by_phase.get("remove"), runners_up_by_phase.get("remove")) + details.get((lang, impl, "remove"), "") + insert_str = format_html_metric(phases.get("insert"), best_by_phase.get("insert"), runners_up_by_phase.get("insert")) + hit_str = format_html_metric(phases.get("hit"), best_by_phase.get("hit"), runners_up_by_phase.get("hit")) + miss_str = format_html_metric(phases.get("miss"), best_by_phase.get("miss"), runners_up_by_phase.get("miss")) + iter_str = format_html_metric(phases.get("iterate"), best_by_phase.get("iterate"), runners_up_by_phase.get("iterate")) + remove_str = format_html_metric(phases.get("remove"), best_by_phase.get("remove"), runners_up_by_phase.get("remove")) table_rows.append( f" " - f"{html.escape(lang)}" + f"{html.escape(format_language(lang))}" f"{html.escape(friendly_name)}" f"{html.escape(structure)}" f'{insert_str}' @@ -894,9 +891,6 @@ def generate_html_report(records, metadata, downloads=()): lines.extend([ " ", - "

Latency by Dataset Size

", - "

The x-axis shows element counts with equal spacing between measured sizes.

", - scaling_charts(records, as_html=True), " ", diff --git a/scripts/test_report.py b/scripts/test_report.py index 0e512ac..3cb51c8 100644 --- a/scripts/test_report.py +++ b/scripts/test_report.py @@ -140,13 +140,18 @@ def test_report_spread_filters_scaling_and_provenance(self): runner_environment="GitHub-hosted CI") markdown = report.generate_report(records, meta) output = report.generate_html_report(records, meta) + self.assertEqual(report.spread_text(records[0]), "IQR 1.50–2.50; 4 samples; 2 forks; fork medians [1.50, 2.50]") for content in (markdown, output): - self.assertIn("IQR 1.50–2.50; 4 samples; 2 forks", content) - self.assertIn("fork medians [1.50, 2.50]", content) + self.assertNotIn("IQR ", content) + self.assertNotIn("fork medians", content) + self.assertNotIn("Latency by Dataset Size", content) self.assertIn("GitHub-hosted CI", content) self.assertIn("/local/ordered", content) - self.assertIn("line [2.00, 2.00]", content) self.assertIn("Seed 123", content) + scaling_md = report.scaling_charts(records) + scaling_html = report.scaling_charts(records, as_html=True) + for content in (scaling_md, scaling_html): + self.assertIn("line [2.00, 2.00]", content) for phase in report.PHASES: self.assertIn(phase.title() + " Scaling", content) for name in ("phase", "kind", "order"): @@ -272,6 +277,8 @@ def test_generate_report(self): self.assertIn("# Benchmark Report: Sorted Maps and Sets", output) self.assertIn("Test CPU", output) self.assertIn("Ordered BTreeMap", output) + self.assertIn("| Zig | Ordered BTreeMap |", output) + self.assertIn("| Rust | std::collections::BTreeMap |", output) # The best insert score (20.00) is bold; the slower insert score (35.00) is not bold. self.assertIn("**20.00**", output) self.assertIn("| 35.00 |", output) @@ -281,6 +288,14 @@ def test_generate_report(self): self.assertNotIn("Sorted Iteration Latency", output) self.assertNotIn("bar [0.00", output) + def test_format_language(self): + self.assertEqual(report.format_language("c"), "C") + self.assertEqual(report.format_language("cpp"), "C++") + self.assertEqual(report.format_language("java"), "Java") + self.assertEqual(report.format_language("rust"), "Rust") + self.assertEqual(report.format_language("zig"), "Zig") + self.assertEqual(report.format_language("unknown"), "Unknown") + def test_format_html_metric(self): self.assertEqual(report.format_html_metric(10.5, 10.5), '10.50') @@ -348,10 +363,17 @@ def test_generate_html_report(self): self.assertIn("Benchmark Report: Sorted Maps and Sets", output) self.assertIn("Test CPU", output) self.assertIn("Ordered BTreeMap", output) + self.assertIn("Zig", output) + self.assertIn("Rust", output) self.assertIn('20.00', output) self.assertIn('35.00', output) self.assertIn('15.00', output) self.assertIn('30.00', output) + self.assertIn('Bold green', output) + self.assertIn('light orange', output) + self.assertIn("
  • Insert:", output) + self.assertIn("
  • Lookup hit:", output) + self.assertIn("
  • B-trees versus binary trees:", output) self.assertIn("xychart-beta", output) self.assertIn('', output) self.assertIn("table.sortable th", output)