From 70cdb439a90a17dc1128a1bad4c265195c30f8f9 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 17:12:36 +1000 Subject: [PATCH 01/13] chore: ignores --- .gitignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3510f13..8e8b085 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ # directories (by name) /_build/ +/_build-ci/ +/scripts/__pycache__/ /scratch/ /target/ @@ -13,8 +15,6 @@ .DS_Store -#Cargo.lock - # files (by pattern) @@ -24,3 +24,4 @@ *.swp *.tmp *.zip + From 48a087ca9e8691b009f3bdbcd3d4b704d5d069ce Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 17:25:33 +1000 Subject: [PATCH 02/13] chore: rustfmt.toml --- rustfmt.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rustfmt.toml b/rustfmt.toml index b1f362b..aca5963 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,5 +1,13 @@ # rustfmt.toml for p99 (p99.Rust) # +# Requires nightly rustfmt with unstable features enabled, e.g.: +# +# ./scripts/fmt +# +# Or: +# +# RUSTFMT="$(rustup which --toolchain nightly rustfmt)" cargo fmt -- --unstable-features +# # Based on rustfmt.toml circa cargo-fmt 1.4.37 era. # edition/style_edition updated for Rust 2021. # Inline comments mark values that differ from the 2021 style edition From e9eceb53cd18fedff4293479e6f7ad4828e86769 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 17:28:33 +1000 Subject: [PATCH 03/13] chore: TODO.md --- TODO.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 2b4e18a..2906085 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,26 @@ -# p99.Rust +# p99.Rust - TODO ## Table of Contents -- [TODOs](#todos) +- [Functional improvements](#functional-improvements) +- [Performance improvements](#performance-improvements) +- [Packaging improvements](#packaging-improvements) -## TODOs +## Functional improvements -- [x] ~~~`Debug` form~~~; -- [ ] binary scaling; +* [x] ~~~`Debug` form~~~ - ✅; + +## Performance improvements + +* [ ] binary scaling; + + +## Packaging improvements + +* \ + + + From 520283df8d82685d425e9a1825d205fa7b7a5b5f Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 17:42:47 +1000 Subject: [PATCH 04/13] chore: .vimrc --- .vimrc | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .vimrc diff --git a/.vimrc b/.vimrc new file mode 100644 index 0000000..d34bf9e --- /dev/null +++ b/.vimrc @@ -0,0 +1,70 @@ +" Synesis C/C++ project .vimrc — aligned with .sis/.vscode/c_cxx/settings.json + +set nocompatible +filetype indent plugin on +syntax enable +set autoindent +set backspace=indent,eol,start +set hlsearch +set incsearch +set number + +" files.insertFinalNewline +set eol +set fixeol + +" editor.renderWhitespace: all +set list +set listchars=tab:->,trail:-,extends:>,precedes:<,nbsp:+ + +" editor.detectIndentation: false — global defaults (editor.tabSize: 2, insertSpaces: false) +set tabstop=2 +set shiftwidth=2 +set softtabstop=2 +set noexpandtab +set colorcolumn=76 + +" colorcolumn draws a full-column tint in Vim (not a VS Code-style 1px line). +" Keep it subtle via the ColorColumn highlight group; reapply after colorscheme changes. +if has('termguicolors') + " set termguicolors +endif + +function! s:ConfigureColorColumn() abort + highlight ColorColumn ctermbg=236 guibg=#2a2a2a cterm=NONE gui=NONE +endfunction + +call s:ConfigureColorColumn() +autocmd ColorScheme * call s:ConfigureColorColumn() + +" files.trimTrailingWhitespace +autocmd BufWritePre * %s/\s\+$//e + +augroup sis_c_cxx + autocmd! + + " [c] / [cpp] + autocmd FileType c,cpp setlocal expandtab tabstop=4 shiftwidth=4 softtabstop=4 colorcolumn=60,64,68,72,76 + + " [rust] + autocmd FileType rs setlocal expandtab tabstop=4 shiftwidth=4 softtabstop=4 colorcolumn=76 + + " [cmake] + autocmd FileType cmake setlocal noexpandtab tabstop=4 shiftwidth=4 softtabstop=4 + + " [shellscript] + autocmd FileType sh,bash,zsh setlocal expandtab tabstop=2 shiftwidth=2 softtabstop=2 colorcolumn=60,76 + + " [bat] + autocmd FileType bat,dosbatch setlocal expandtab tabstop=4 shiftwidth=4 softtabstop=4 colorcolumn=60,76 + + " [json] / [markdown] / [yaml] / [ruby] + autocmd FileType json,markdown,yaml,ruby setlocal expandtab tabstop=2 shiftwidth=2 softtabstop=2 + + " [python] + autocmd FileType python setlocal expandtab tabstop=4 shiftwidth=4 softtabstop=4 colorcolumn=60,76 + + " [toml] + autocmd FileType toml setlocal noexpandtab tabstop=2 shiftwidth=2 softtabstop=2 +augroup END + From d596333c11b639c804235b59de874d1a6d04f241 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 18:02:55 +1000 Subject: [PATCH 05/13] chore: EXAMPLES.md --- EXAMPLES.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 EXAMPLES.md diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..113704d --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,6 @@ +# p99.Rust Examples + + + + + From 2bacd843982625039daf3e224051cdde056d24a0 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 18:05:59 +1000 Subject: [PATCH 06/13] chore: .gitattributes --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitattributes b/.gitattributes index 7beaa80..a74c4ff 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,6 @@ *.rs linguist-language=Rust + +*.html linguist-detectable=false +*.sh linguist-detectable=false +*.vimrc linguist-detectable=false From b9b76b9659f712b0bb9a7bbb50b181c9c1a81bbb Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 18:19:51 +1000 Subject: [PATCH 07/13] chore: VS Code settings --- .vscode/settings.json | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 577e52d..f1f32fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,38 +1,44 @@ { "[json]": { - "editor.tabSize": 2 + "editor.insertSpaces": false, + "editor.tabSize": 2, + }, + "[markdown]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, }, "[python]": { "editor.insertSpaces": true, "editor.rulers": [ 60, 76 ], - "editor.tabSize": 2 + "editor.tabSize": 4, }, "[ruby]": { "editor.insertSpaces": true, "editor.rulers": [ 60, 76 ], - "editor.tabSize": 2 + "editor.tabSize": 2, }, "[rust]": { "editor.insertSpaces": true, "editor.rulers": [ 60, 76 ], - "editor.tabSize": 4 + "editor.tabSize": 4, }, "[toml]": { "editor.insertSpaces": false, - "editor.tabSize": 2 + "editor.tabSize": 2, }, "cmake.configureOnOpen": false, "editor.detectIndentation": false, "editor.insertSpaces": false, "editor.renderWhitespace": "all", + "editor.rulers": [ 76 ], "editor.tabSize": 2, "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "git.mergeEditor": false, "rust-analyzer.cargo.noDefaultFeatures": true, "rust-analyzer.cargo.features": [ - "default" + "default", ], "rust-analyzer.completion.autoimport.enable": false, - "rust-analyzer.showUnlinkedFileNotification": false + "rust-analyzer.showUnlinkedFileNotification": false, } From 84f894da722ba1c510f7e69f7047554f69ed021a Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 18:34:04 +1000 Subject: [PATCH 08/13] chore: NEWS.md --- NEWS.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 NEWS.md diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..8cf9981 --- /dev/null +++ b/NEWS.md @@ -0,0 +1,8 @@ +# p99.Rust - NEWS + +| Date | News Item | +| --------------------- | ----------------------------------------- | + + + + From 6fe22f7c65362419c974ca3ca9e15f48082f1ec5 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Wed, 8 Jul 2026 19:46:04 +1000 Subject: [PATCH 09/13] chore: chore: rustfmt.toml --- rustfmt.toml | 137 +++++++++++++++++++++++++++++---------------------- 1 file changed, 78 insertions(+), 59 deletions(-) diff --git a/rustfmt.toml b/rustfmt.toml index aca5963..9dbe787 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,3 +1,4 @@ + # rustfmt.toml for p99 (p99.Rust) # # Requires nightly rustfmt with unstable features enabled, e.g.: @@ -13,63 +14,81 @@ # Inline comments mark values that differ from the 2021 style edition # default. -edition = "2021" -style_edition = "2021" +# array_width=60 # deprecated +# attr_fn_like_width=70 # deprecated +binop_separator="Front" +blank_lines_lower_bound=0 +blank_lines_upper_bound=3 +brace_style="SameLineWhere" +# chain_width=60 # deprecated +color="Auto" +combine_control_expr=false +comment_width=76 +condense_wildcard_suffixes=false +control_brace_style="AlwaysSameLine" +disable_all_formatting=false +edition="2021" +empty_item_single_line=false +enum_discrim_align_threshold=0 +error_on_line_overflow=false +error_on_unformatted=false +# fn_args_layout="Vertical" +# fn_call_width=60 # deprecated +fn_params_layout="Vertical" +fn_single_line=false +force_explicit_abi=true +force_multiline_blocks=true +# force_code_in_doc_comments=true +format_macro_bodies=true +format_macro_matchers=true +format_strings=false +group_imports="Preserve" +hard_tabs=false +# hide_parse_errors=false +ignore=[ +] +imports_granularity="Crate" +imports_indent="Block" +imports_layout="Vertical" +indent_style="Block" +inline_attribute_width=0 +# license_template_path="" +match_arm_blocks=true +match_arm_leading_pipes="Preserve" +match_block_trailing_comma=true +max_width=120 +merge_derives=false +# merge_imports= # deprecated +newline_style="Auto" +normalize_comments=false +normalize_doc_attributes=true +overflow_delimited_expr=false +remove_nested_parens=true +reorder_impl_items=true +reorder_imports=true +reorder_modules=true +# report_fixme="Always" +# report_todo="Never" +# required_version="????" +short_array_element_width_threshold=1 +# single_line_if_else_max_width=0 # deprecated +show_parse_errors=true +skip_children=false +space_after_colon=true +space_before_colon=true +spaces_around_ranges=false +struct_field_align_threshold=20 +struct_lit_single_line=false +style_edition="2021" +tab_spaces=4 +trailing_comma="Vertical" +trailing_semicolon=true +type_punctuation_density="Wide" +unstable_features=true +use_field_init_shorthand=true +use_small_heuristics="Default" +use_try_shorthand=true +# version= +where_single_line=false +wrap_comments=false -binop_separator = "Front" -blank_lines_lower_bound = 0 -blank_lines_upper_bound = 3 # default: 1 -brace_style = "SameLineWhere" -color = "Auto" -combine_control_expr = false # default: true -comment_width = 100 # default: 80 -condense_wildcard_suffixes = false -control_brace_style = "AlwaysSameLine" -disable_all_formatting = false -empty_item_single_line = false # default: true -enum_discrim_align_threshold = 0 -error_on_line_overflow = false -error_on_unformatted = false -fn_params_layout = "Vertical" # default: "Tall" -fn_single_line = false -force_explicit_abi = true -force_multiline_blocks = true # default: false -format_macro_bodies = true -format_macro_matchers = true # default: false -format_strings = false -group_imports = "Preserve" -hard_tabs = false -ignore = [] -imports_granularity = "Crate" # default: "Preserve" -imports_indent = "Block" -imports_layout = "Vertical" # default: "Mixed" -indent_style = "Block" -inline_attribute_width = 0 -match_arm_blocks = true -match_arm_leading_pipes = "Preserve" # default: "Never" -match_block_trailing_comma = true # default: false -max_width = 120 # default: 100 -merge_derives = false # default: true -newline_style = "Auto" -normalize_comments = false -normalize_doc_attributes = true # default: false -overflow_delimited_expr = false -remove_nested_parens = true -reorder_impl_items = true # default: false -reorder_imports = true -reorder_modules = true -skip_children = false -space_after_colon = true -space_before_colon = true # default: false -spaces_around_ranges = false -struct_field_align_threshold = 40 # default: 0 -struct_lit_single_line = false # default: true -tab_spaces = 4 -trailing_comma = "Vertical" -trailing_semicolon = true -type_punctuation_density = "Wide" -use_field_init_shorthand = true # default: false -use_small_heuristics = "Default" -use_try_shorthand = true # default: false -where_single_line = false -wrap_comments = false From b8b1554faed720ece1232ef7d6bb1cbe06c947ce Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Thu, 9 Jul 2026 16:52:34 +1000 Subject: [PATCH 10/13] chore: cargo update --- Cargo.lock | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb2ab3d..ce30df3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,12 @@ version = "0.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb2de59cca22f51f3301a09c549b0b4a32f56e8a007d0d44b02a1ce42975b37" +[[package]] +name = "bt-rs" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4985bd0bdfcd2a13bdeaa1798240f2168998107a7939a39384c4c7608392a26" + [[package]] name = "cast" version = "0.3.0" @@ -52,9 +58,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.65" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "shlex", @@ -203,9 +209,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "num-traits" @@ -358,11 +364,12 @@ dependencies = [ [[package]] name = "test_help-rs" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d2df2cc6a57f53f03c47cc937df746b8f2d0f5e63b77b7d81aa30b9ceb2842" +checksum = "860ba0c1f6ec76a108a38b90425e38a84d024597cfe4deacb4f19f5a9839541d" dependencies = [ "base-traits", + "bt-rs", ] [[package]] @@ -439,18 +446,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", From 58bfa3fbd3bbf0c000862583886cb858fb8edcb3 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Mon, 13 Jul 2026 08:07:58 +1000 Subject: [PATCH 11/13] squash-commit --- CHANGES.md | 11 +++++++ Cargo.lock | 2 +- Cargo.toml | 20 +++++++++++-- README.md | 84 ++++++++++++++++++++++++++++++++++++++++++------------ TODO.md | 5 ++-- src/lib.rs | 72 +++++++++++++++++++++++++++++++++++----------- 6 files changed, 155 insertions(+), 39 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c947631..3b48519 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,17 @@ # p99.Rust CHANGES +## 0.0.3 - 14th July 2026 + +* T.B.C.; + + +## 0.0.2 - 13th July 2026 + +* added opt-in crate feature **`"binary-scaling"`** that replaces integer division with $2^{32}$ fixed-point binary scaling for all integer-based percentile queries (`value_at_p90()`, `value_at_p95()`, `value_at_p99()`, etc.), achieving a ~1.5x to 2x speedup with a small loss of accuracy; +* added **`"null-feature"`** -- a no-op feature that has no effect but simplifies driver scripts that conditionally pass features; + + ## 0.0.1 - 26th June 2026 FIRST PUBLIC RELEASE diff --git a/Cargo.lock b/Cargo.lock index ce30df3..0626e91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,7 +230,7 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "p99" -version = "0.0.1" +version = "0.0.3" dependencies = [ "criterion", "test_help-rs", diff --git a/Cargo.toml b/Cargo.toml index 97d3c7d..70d9f92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ [package] authors = [ + "Harry Wilson ", "Matt Wilson ", ] categories = [ @@ -23,7 +24,7 @@ license = "BSD-3-Clause" name = "p99" readme = "README.md" repository = "https://github.com/synesissoftware/p99.Rust" -version = "0.0.1" +version = "0.0.3" # ########################################################## @@ -47,7 +48,22 @@ path = "examples/build_histogram.rs" [features] -default = [] +default = [ +] + +# General features: +# +# - "null-feature" - a feature that has no effect (and, thus, is useful for simplifying driver scripts); + +null-feature = [] + +# Crate-specific features: +# +# - "binary-scaling" - uses binary scaling to calculate target rank, achieving higher performance with a small loss of accuracy; + + +binary-scaling = [ +] # ########################################################## diff --git a/README.md b/README.md index 3fdea9b..63e0c73 100644 --- a/README.md +++ b/README.md @@ -14,27 +14,29 @@ Low-cost generation of performance percentiles (p50, p90, p99, p99.9, etc.). - [Introduction](#introduction) - [How It Works](#how-it-works) - [Performance \& Trade-offs](#performance--trade-offs) - - [Performance Claims](#performance-claims) - - [Trade-offs \& Sacrifices](#trade-offs--sacrifices) + - [Performance Claims](#performance-claims) + - [Trade-offs \& Sacrifices](#trade-offs--sacrifices) - [Installation](#installation) - [Components](#components) - - [Constants](#constants) - - [Enumerations](#enumerations) - - [Features](#features) - - [Functions](#functions) - - [Macros](#macros) - - [Structures](#structures) - - [`Histogram`](#histogram) - - [Definition](#definition) - - [Minimal Example](#minimal-example) - - [Traits](#traits) + - [Constants](#constants) + - [Enumerations](#enumerations) + - [Features](#features) + - [Enabling `binary-scaling`](#enabling-binary-scaling) + - [Benchmark Results](#benchmark-results) + - [Functions](#functions) + - [Macros](#macros) + - [Structures](#structures) + - [`Histogram`](#histogram) + - [Definition](#definition) + - [Minimal Example](#minimal-example) + - [Traits](#traits) - [Examples](#examples) - [Project Information](#project-information) - - [Where to get help](#where-to-get-help) - - [Contribution guidelines](#contribution-guidelines) - - [Dependencies](#dependencies) - - [Dev Dependencies](#dev-dependencies) - - [License](#license) + - [Where to get help](#where-to-get-help) + - [Contribution guidelines](#contribution-guidelines) + - [Dependencies](#dependencies) + - [Dev Dependencies](#dev-dependencies) + - [License](#license) ## Introduction @@ -70,6 +72,7 @@ Low-cost generation of performance percentiles (p50, p90, p99, p99.9, etc.). * **Logarithmic Precision**: To achieve zero allocation and constant-time operations, `Histogram` sacrifices exact precision. It does not store individual event times. Instead, values are grouped into logarithmic buckets. * **Approximation**: Percentile values are approximated using linear interpolation within the bucket boundaries. For very large values, the bucket width is wider, which leads to a wider approximation range. However, for low-latency performance measurements where precision is needed most (the lower nanosecond ranges), the buckets are extremely narrow (e.g., 1ns, 2ns, 4ns wide), providing exceptional resolution. +* **`binary-scaling` Accuracy**: When the `binary-scaling` feature is enabled, the percentile target rank is computed using a $2^{32}$ fixed-point approximation. The pre-encoded multiplier for each percentile (e.g., `3_865_470_566 >> 32` ≈ `0.9000` for p90) differs from the true decimal value by less than $10^{-9}$, which is far below the approximation error introduced by the logarithmic bucketing itself. In practice this has no measurable impact on percentile accuracy. ## Installation @@ -80,6 +83,12 @@ Reference in **Cargo.toml** in the usual way: p99 = { version = "0" } ``` +To enable the optional binary-scaling optimization: + +```toml +p99 = { version = "0", features = ["binary-scaling"] } +``` + ## Components @@ -95,7 +104,43 @@ No public enumerations are defined at this time. ### Features -No public crate-specific features are defined at this time. +The following crate features are available: + +* **`binary-scaling`** *(opt-in)*: Replaces integer division in the integer-based percentile methods (`value_at_p90()`, `value_at_p95()`, `value_at_p99()`, etc.) with $2^{32}$ fixed-point binary scaling. Each percentile multiplier (e.g., `0.90` for p90) is pre-encoded as a `u32` constant and the target rank is computed via a single multiplication and a 32-bit right-shift, avoiding the cost of integer division entirely. This yields a **~1.5x to 2x speedup** for percentile queries with a negligible loss of accuracy (the scaled multiplier differs from the true value by less than $10^{-9}$). The generic `value_at_percentile(f64)` method is unaffected by this feature; + +* **`null-feature`** *(opt-in)*: A no-op feature that has no effect on the compiled library. It exists to simplify driver scripts and CI pipelines that conditionally pass `--features` flags, allowing a feature list to always be present even when no real features are needed; + +#### Enabling `binary-scaling` + +Add the feature in your **Cargo.toml**: + +```toml +[dependencies] +p99 = { version = "0", features = ["binary-scaling"] } +``` + +Or, when building from the command line: + +```bash +# Default (standard integer division) +cargo run --example build_histogram + +# With binary scaling enabled +cargo run --example build_histogram --features binary-scaling +``` + +#### Benchmark Results + +Measured with [**criterion**](https://github.com/bheisler/criterion.rs) on 100k events (Apple M-series, release profile). Only the integer-based percentile methods are affected; the generic `value_at_percentile(f64)` method is unchanged. + +| Method | Default | `binary-scaling` | Improvement | +|---|---:|---:|---:| +| `value_at_p90()` | 23.25 ns | 21.63 ns | **-7.0%** | +| `value_at_p99()` (dense) | 16.52 ns | 14.88 ns | **-10.4%** | +| `value_at_p99()` (wide) | 23.39 ns | 21.55 ns | **-7.9%** | +| `value_at_p99_99()` | 23.32 ns | 21.64 ns | **-7.2%** | + +Methods using simple fractional multipliers (p50 = 1/2, p75 = 3/4) already compile to bit-shifts without this feature, so they show no change. ### Functions @@ -184,6 +229,9 @@ cargo run --example build_histogram # Run with 1000 tries P99_TRIES=1000 cargo run --example build_histogram + +# Run with binary-scaling enabled (faster percentile queries) +cargo run --example build_histogram --features binary-scaling ``` diff --git a/TODO.md b/TODO.md index 2906085..df6990d 100644 --- a/TODO.md +++ b/TODO.md @@ -10,12 +10,13 @@ ## Functional improvements -* [x] ~~~`Debug` form~~~ - ✅; +- [x] ~~~`Debug` form~~~; +- [x] ~~~binary scaling~~~; ## Performance improvements -* [ ] binary scaling; +- [x] ~~~binary scaling~~~; ## Packaging improvements diff --git a/src/lib.rs b/src/lib.rs index 8b38639..3e07d27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -295,8 +295,13 @@ impl Histogram { /// events; otherwise, returns `None`. #[inline(always)] pub fn value_at_p90(&self) -> Option { - let target_rank = (self.event_count as u128 * 90) / 100; - let r = self.value_at_target_rank_impl(target_rank as u64); + #[cfg(feature = "binary-scaling")] + let target_rank = ((self.event_count as u128 * 3_865_470_566) >> 32) as u64; // multiplier: 0.8999999999068677 + + #[cfg(not(feature = "binary-scaling"))] + let target_rank = ((self.event_count as u128 * 90) / 100) as u64; + + let r = self.value_at_target_rank_impl(target_rank); r } @@ -309,8 +314,13 @@ impl Histogram { /// events; otherwise, returns `None`. #[inline(always)] pub fn value_at_p95(&self) -> Option { - let target_rank = (self.event_count as u128 * 95) / 100; - let r = self.value_at_target_rank_impl(target_rank as u64); + #[cfg(feature = "binary-scaling")] + let target_rank = ((self.event_count as u128 * 4_080_218_931) >> 32) as u64; // multiplier: 0.9499999999534339 + + #[cfg(not(feature = "binary-scaling"))] + let target_rank = ((self.event_count as u128 * 95) / 100) as u64; + + let r = self.value_at_target_rank_impl(target_rank); r } @@ -323,8 +333,13 @@ impl Histogram { /// events; otherwise, returns `None`. #[inline(always)] pub fn value_at_p99(&self) -> Option { - let target_rank = (self.event_count as u128 * 99) / 100; - let r = self.value_at_target_rank_impl(target_rank as u64); + #[cfg(feature = "binary-scaling")] + let target_rank = ((self.event_count as u128 * 4_252_017_623) >> 32) as u64; // multiplier: 0.9899999999906868 + + #[cfg(not(feature = "binary-scaling"))] + let target_rank = ((self.event_count as u128 * 99) / 100) as u64; + + let r = self.value_at_target_rank_impl(target_rank); r } @@ -337,8 +352,13 @@ impl Histogram { /// events; otherwise, returns `None`. #[inline(always)] pub fn value_at_p99_5(&self) -> Option { - let target_rank = (self.event_count as u128 * 995) / 1_000; - let r = self.value_at_target_rank_impl(target_rank as u64); + #[cfg(feature = "binary-scaling")] + let target_rank = ((self.event_count as u128 * 4_273_492_460) >> 32) as u64; // multiplier: 0.9950000001117587 + + #[cfg(not(feature = "binary-scaling"))] + let target_rank = ((self.event_count as u128 * 995) / 1_000) as u64; + + let r = self.value_at_target_rank_impl(target_rank); r } @@ -351,8 +371,13 @@ impl Histogram { /// events; otherwise, returns `None`. #[inline(always)] pub fn value_at_p99_9(&self) -> Option { - let target_rank = (self.event_count as u128 * 999) / 1_000; - let r = self.value_at_target_rank_impl(target_rank as u64); + #[cfg(feature = "binary-scaling")] + let target_rank = ((self.event_count as u128 * 4_290_672_329) >> 32) as u64; // multiplier: 0.9990000000689179 + + #[cfg(not(feature = "binary-scaling"))] + let target_rank = ((self.event_count as u128 * 999) / 1_000) as u64; + + let r = self.value_at_target_rank_impl(target_rank); r } @@ -365,8 +390,13 @@ impl Histogram { /// events; otherwise, returns `None`. #[inline(always)] pub fn value_at_p99_99(&self) -> Option { - let target_rank = (self.event_count as u128 * 9_999) / 10_000; - let r = self.value_at_target_rank_impl(target_rank as u64); + #[cfg(feature = "binary-scaling")] + let target_rank = ((self.event_count as u128 * 4_294_537_799) >> 32) as u64; // multiplier: 0.9998999999370426 + + #[cfg(not(feature = "binary-scaling"))] + let target_rank = ((self.event_count as u128 * 9_999) / 10_000) as u64; + + let r = self.value_at_target_rank_impl(target_rank); r } @@ -379,8 +409,13 @@ impl Histogram { /// events; otherwise, returns `None`. #[inline(always)] pub fn value_at_p99_999(&self) -> Option { - let target_rank = (self.event_count as u128 * 99_999) / 100_000; - let r = self.value_at_target_rank_impl(target_rank as u64); + #[cfg(feature = "binary-scaling")] + let target_rank = ((self.event_count as u128 * 4_294_924_346) >> 32) as u64; // multiplier: 0.9999899999238551 + + #[cfg(not(feature = "binary-scaling"))] + let target_rank = ((self.event_count as u128 * 99_999) / 100_000) as u64; + + let r = self.value_at_target_rank_impl(target_rank); r } @@ -393,8 +428,13 @@ impl Histogram { /// events; otherwise, returns `None`. #[inline(always)] pub fn value_at_p99_999_9(&self) -> Option { - let target_rank = (self.event_count as u128 * 999_999) / 1_000_000; - let r = self.value_at_target_rank_impl(target_rank as u64); + #[cfg(feature = "binary-scaling")] + let target_rank = ((self.event_count as u128 * 4_294_963_001) >> 32) as u64; // multiplier: 0.9999989999923855 + + #[cfg(not(feature = "binary-scaling"))] + let target_rank = ((self.event_count as u128 * 999_999) / 1_000_000) as u64; + + let r = self.value_at_target_rank_impl(target_rank); r } From 3cce9344aaf60fae18505580b9f620efe7a60421 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Mon, 13 Jul 2026 08:17:06 +1000 Subject: [PATCH 12/13] boilerplate --- CHANGES.md | 2 +- NEWS.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 215eba4..e2bc688 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,7 @@ ## 0.0.3 - 14th July 2026 -* T.B.C.; +* standardised project boilerplate: (**.gitattributes**, **.gitignore**, **.vimrc**, **.vscode/settings.json**); project files (**CHANGES.md**, **EXAMPLES.md**, **NEWS.md**, **README.md**, **TODO.md**); ## 0.0.2 - 13th July 2026 diff --git a/NEWS.md b/NEWS.md index 8cf9981..790b5ad 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,9 @@ | Date | News Item | | --------------------- | ----------------------------------------- | +| 14th July 2026 | p99.Rust 0.0.3 released | +| 13th July 2026 | p99.Rust 0.0.2 released | +| 26th June 2026 | p99.Rust 0.0.1 released | From 91b866fda2aca2e097ff30910c0c0a00b1ec77c1 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Tue, 14 Jul 2026 11:47:58 +1000 Subject: [PATCH 13/13] chore: improved example(s) --- EXAMPLES.md | 5 +- examples/build_histogram.md | 161 ++++++++++++++++++++++++++++++++++++ examples/build_histogram.rs | 26 +++++- 3 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 examples/build_histogram.md diff --git a/EXAMPLES.md b/EXAMPLES.md index 113704d..f9dd78d 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,6 +1,9 @@ # p99.Rust Examples +|Name|Source & Description|Summary| +|---|---|---| +|**build_histogram**|[examples/build_histogram/main.rs](/examples/build_histogram/main.rs)
[examples/build_histogram.md](/examples/build_histogram.md)|Lists files in the current directory whose paths match one or more shell wildcard pattern(s), using `shwild::matches()`.| - + diff --git a/examples/build_histogram.md b/examples/build_histogram.md new file mode 100644 index 0000000..d9de094 --- /dev/null +++ b/examples/build_histogram.md @@ -0,0 +1,161 @@ +# `build_histogram.rs` + +This example demonstrates the usage of the `Histogram` struct from the `p99` crate to measure and display event durations. + +The program simulates a series of events, each with a random delay, and records their durations using a `Histogram` instance. After all events are processed, it prints the histogram in a detailed format and also calculates and displays various percentiles (p50, p75, p90, p95, p99, p99.5, p99.9, p99.99) of the recorded durations. + +The number of tries (simulated events) can be controlled by setting the `P99_TRIES` environment variable. If not set, it defaults to 100. + +```rust +// examples/build_histogram.rs : example program showing `Histogram` + +use p99::Histogram; + +#[rustfmt::skip] +use std::{ + env as std_env, + thread as std_thread, + time as std_time, +}; + +struct SimpleRng { + state: u64, +} + +// API functions + +impl SimpleRng { + fn new(seed: u64) -> Self { + Self { state: seed } + } +} + +// Mutating methods + +impl SimpleRng { + fn next(&mut self) -> u64 { + self.state = self.state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1); + + self.state + } +} + +// Helper functions + +fn main() { + let range = match std_env::var("P99_RANGE") { + Ok(val) => match val.parse::() { + Ok(num) => num, + Err(_) => { + eprintln!("Warning: failed to parse P99_RANGE value '{}', defaulting to 1,000,000", val); + + 1_000_000 + }, + }, + Err(_) => 1_000_000, + }; + let tries = match std_env::var("P99_TRIES") { + Ok(val) => match val.parse::() { + Ok(num) => num, + Err(_) => { + eprintln!("Warning: failed to parse P99_TRIES value '{}', defaulting to 100", val); + + 100 + }, + }, + Err(_) => 100, + }; + + println!("Running Histogram example with {} tries...", tries); + + let mut histogram = Histogram::default(); + let mut rng = SimpleRng::new(12_345); + + for _ in 0..tries { + // Generate a random delay from 1 to nanoseconds. + let delay_ns = (rng.next() % range as u64) + 1; + let start = std_time::Instant::now(); + + if delay_ns < 1_000_000 { + + let busy_wait_start = std_time::Instant::now(); + while busy_wait_start.elapsed().as_nanos() < delay_ns as u128 { + std::hint::black_box(1); + } + } else { + + std_thread::sleep(std_time::Duration::from_micros(delay_ns)); + } + + let elapsed = start.elapsed(); + histogram.push_event_duration(elapsed); + } + + println!("\nHistogram printed via `{{:#?}}` format:\n"); + println!("{:#?}", histogram); + + println!("\nPercentiles (approximated):"); + println!(" p50 (f64): {:?} ns", histogram.value_at_percentile(50.0)); + println!(" p50 (integer): {:?} ns", histogram.value_at_p50()); + println!(" p75 (integer): {:?} ns", histogram.value_at_p75()); + println!(" p90 (integer): {:?} ns", histogram.value_at_p90()); + println!(" p95 (integer): {:?} ns", histogram.value_at_p95()); + println!(" p99 (integer): {:?} ns", histogram.value_at_p99()); + println!(" p99.5 (integer): {:?} ns", histogram.value_at_p99_5()); + println!(" p99.9 (integer): {:?} ns", histogram.value_at_p99_9()); + println!(" p99.99 (integer): {:?} ns", histogram.value_at_p99_99()); +} +``` + +Example output: + +``` +$ P99_RANGE=1000000 P99_TRIES=10000 cargo run --release --example build_histogram + Finished `release` profile [optimized] target(s) in 0.09s + Running `target/release/examples/build_histogram` +Running Histogram example with 10000 tries... + +Histogram printed via `{:#?}` format: + +Histogram { + event_count: 10000, + event_time_total: Some( + 5009212642, + ), + has_overflowed: false, + min_event_time: Some( + 125, + ), + max_event_time: Some( + 2332375, + ), + buckets: { + "2^6": 1, + "2^7": 4, + "2^8": 4, + "2^9": 6, + "2^10": 14, + "2^11": 23, + "2^12": 49, + "2^13": 87, + "2^14": 166, + "2^15": 306, + "2^16": 658, + "2^17": 1284, + "2^18": 2627, + "2^19": 4770, + "2^21": 1, + }, +} + +Percentiles (approximated): + p50 (f64): Some(501436) ns + p50 (integer): Some(501435) ns + p75 (integer): Some(773901) ns + p90 (integer): Some(938771) ns + p95 (integer): Some(993728) ns + p99 (integer): Some(1037693) ns + p99.5 (integer): Some(1043189) ns + p99.9 (integer): Some(1047585) ns + p99.99 (integer): Some(1048575) ns +``` diff --git a/examples/build_histogram.rs b/examples/build_histogram.rs index f02eeec..2165146 100644 --- a/examples/build_histogram.rs +++ b/examples/build_histogram.rs @@ -34,6 +34,17 @@ impl SimpleRng { // Helper functions fn main() { + let range = match std_env::var("P99_RANGE") { + Ok(val) => match val.parse::() { + Ok(num) => num, + Err(_) => { + eprintln!("Warning: failed to parse P99_RANGE value '{}', defaulting to 1,000,000", val); + + 1_000_000 + }, + }, + Err(_) => 1_000_000, + }; let tries = match std_env::var("P99_TRIES") { Ok(val) => match val.parse::() { Ok(num) => num, @@ -52,11 +63,20 @@ fn main() { let mut rng = SimpleRng::new(12_345); for _ in 0..tries { - // Generate a random delay from 1 to 500 microseconds. - let delay_us = (rng.next() % 1_000) + 1; + // Generate a random delay from 1 to nanoseconds. + let delay_ns = (rng.next() % range as u64) + 1; let start = std_time::Instant::now(); - std_thread::sleep(std_time::Duration::from_micros(delay_us)); + if delay_ns < 1_000_000 { + + let busy_wait_start = std_time::Instant::now(); + while busy_wait_start.elapsed().as_nanos() < delay_ns as u128 { + std::hint::black_box(1); + } + } else { + + std_thread::sleep(std_time::Duration::from_micros(delay_ns)); + } let elapsed = start.elapsed(); histogram.push_event_duration(elapsed);