From e40116b6d70157e7393fd2b64b28c01d82049a74 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Mon, 12 Oct 2026 11:17:19 +0300 Subject: [PATCH 1/7] new performance tests --- performance/rust_app/.gitignore | 3 + performance/rust_app/Cargo.lock | 111 ++++++ performance/rust_app/Cargo.toml | 16 +- performance/rust_app/README.md | 122 +++++++ performance/rust_app/VALIDATION.md | 18 + performance/rust_app/build.rs | 166 +++++++++ performance/rust_app/run.sh | 5 +- performance/rust_app/src/config.rs | 308 ++++++++++++++++ performance/rust_app/src/main.rs | 335 ++++------------- performance/rust_app/src/test/data.rs | 135 +++++++ performance/rust_app/src/test/mod.rs | 241 ++++++++++++ performance/rust_app/src/test/runner.rs | 345 ++++++++++++++++++ .../rust_app/src/test/test_0_checkerboard.rs | 54 +-- .../rust_app/src/test/test_10_diagonal.rs | 28 ++ .../rust_app/src/test/test_11_collinear.rs | 21 ++ .../rust_app/src/test/test_12_cluster.rs | 24 ++ .../rust_app/src/test/test_13_seeded.rs | 28 ++ .../rust_app/src/test/test_1_not_overlap.rs | 58 +-- .../rust_app/src/test/test_2_lines_net.rs | 41 +-- .../rust_app/src/test/test_3_spiral.rs | 81 ++-- .../rust_app/src/test/test_4_windows.rs | 41 +-- .../src/test/test_5_nested_squares.rs | 36 +- .../rust_app/src/test/test_6_corrosion.rs | 42 +-- .../rust_app/src/test/test_7_concentric.rs | 37 +- .../rust_app/src/test/test_8_wind_mill.rs | 49 +-- .../rust_app/src/test/test_9_orientation.rs | 23 ++ performance/rust_app/src/test/util.rs | 22 -- 27 files changed, 1765 insertions(+), 625 deletions(-) create mode 100644 performance/rust_app/.gitignore create mode 100644 performance/rust_app/Cargo.lock create mode 100644 performance/rust_app/README.md create mode 100644 performance/rust_app/VALIDATION.md create mode 100644 performance/rust_app/build.rs create mode 100644 performance/rust_app/src/config.rs create mode 100644 performance/rust_app/src/test/data.rs create mode 100644 performance/rust_app/src/test/runner.rs create mode 100644 performance/rust_app/src/test/test_10_diagonal.rs create mode 100644 performance/rust_app/src/test/test_11_collinear.rs create mode 100644 performance/rust_app/src/test/test_12_cluster.rs create mode 100644 performance/rust_app/src/test/test_13_seeded.rs create mode 100644 performance/rust_app/src/test/test_9_orientation.rs diff --git a/performance/rust_app/.gitignore b/performance/rust_app/.gitignore new file mode 100644 index 00000000..0fc402cb --- /dev/null +++ b/performance/rust_app/.gitignore @@ -0,0 +1,3 @@ +# This executable pins its dependency resolution independently of the library. +!Cargo.lock +results/ diff --git a/performance/rust_app/Cargo.lock b/performance/rust_app/Cargo.lock new file mode 100644 index 00000000..97870ea3 --- /dev/null +++ b/performance/rust_app/Cargo.lock @@ -0,0 +1,111 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "i_float" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecaff1ce29bcacd72cdf581ba6fce636ef4c4e0b2da7e7846dd0137d98141e2" +dependencies = [ + "libm", +] + +[[package]] +name = "i_key_sort" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c6c58d0c60705e66264ce0f788a69a2f21472aeb8188559e7c8c619dbdc10fa" +dependencies = [ + "rayon", +] + +[[package]] +name = "i_overlay" +version = "9.0.1" +dependencies = [ + "i_float", + "i_key_sort", + "i_shape", + "i_tree", + "rayon", +] + +[[package]] +name = "i_shape" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa1878f91ef7921feaf1b2346253ebc5e661878b22d5715195c1d5dcf0f20ae5" +dependencies = [ + "i_float", +] + +[[package]] +name = "i_tree" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9d4a992a9fe83130f41ceacceac3bb116a4355dfc9c8d6ecea4f1e4b4c6caf" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rust_app" +version = "0.2.0" +dependencies = [ + "i_overlay", +] diff --git a/performance/rust_app/Cargo.toml b/performance/rust_app/Cargo.toml index efe8ac11..9469d194 100644 --- a/performance/rust_app/Cargo.toml +++ b/performance/rust_app/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "rust_app" -version = "0.1.0" +version = "0.2.0" edition = "2021" +rust-version = "1.88" +default-run = "rust_app" [profile.release] opt-level = 3 lto = false codegen-units = 1 -[dependencies] +[features] +default = [] +multithreading = ["i_overlay/allow_multithreading"] -#i_overlay = { path = "../../iOverlay", default-features = true } -#i_overlay = { path = "../../iOverlay", features = ["allow_multithreading"] } -#i_tree = { path = "../../../iTree" } -i_overlay = "7.0.0" -i_key_sort = "^0.10.1" -i_tree = "^0.19.0" \ No newline at end of file +[dependencies] +i_overlay = { path = "../../iOverlay", version = "9.0.1" } diff --git a/performance/rust_app/README.md b/performance/rust_app/README.md new file mode 100644 index 00000000..70c168e8 --- /dev/null +++ b/performance/rust_app/README.md @@ -0,0 +1,122 @@ +# Performance-тесты локальной iOverlay + +Стенд 0.2.0 подключает локальную iOverlay 9.0.1 через `../../iOverlay`. Прежний подход сохранён: один сценарий — один `src/test/test_N_name.rs`, запуск по номеру через **`--test N`**. Номера 0..8 и исходная геометрия этих тестов сохранены. Отдельного legacy-бинарника нет. + +## Набор тестов + +| Номер | Файл в src/test | Нагрузка / варианты | +|---:|---|---| +| 0 | test_0_checkerboard.rs | Исходная сетка перекрывающихся квадратов, Xor | +| 1 | test_1_not_overlap.rs | Исходная сетка разнесённых квадратов, Union; `--layout line` размещает квадраты вдоль X с O(1) активных рёбер | +| 2 | test_2_lines_net.rs | Исходные горизонтальные/вертикальные полосы, плотная сетка пересечений, Intersect | +| 3 | test_3_spiral.rs | Исходная float-спираль, Subject; `--spiral integer` — детерминированная ортогональная лента с центральным вырезом, Difference | +| 4 | test_4_windows.rs | Исходные окна/отверстия, Difference | +| 5 | test_5_nested_squares.rs | Исходные концентрические прямоугольные рамки, Xor | +| 6 | test_6_corrosion.rs | Исходные сетки многоугольников разных размеров, Difference | +| 7 | test_7_concentric.rs | Исходные концентрические многоугольники с многочисленными клипами, Intersect | +| 8 | test_8_wind_mill.rs | Исходная сетка «мельниц», Intersect | +| **9** | **test_9_orientation.rs** | Длинные перекрывающиеся полосы; `--orientation horizontal/vertical/all`, точный поворот на 90°, Xor | +| **10** | **test_10_diagonal.rs** | Длинные почти параллельные диагональные полосы, мало пересечений, Intersect | +| **11** | **test_11_collinear.rs** | Частичные коллинеарные наложения и общие границы, Xor | +| **12** | **test_12_cluster.rs** | Плотный seeded-кластер; `--outlier false/true/all` перемещает один квадрат внутри кластера или далеко от него, Union | +| **13** | **test_13_seeded.rs** | Детерминированные псевдослучайные прямоугольники, Xor | + +Отдельные новые checkerboard, disjoint, grid, nested, windows убраны: соответствующие нагрузки уже представлены номерами 0,1,2,5/7,4. Целочисленная спираль и линейное размещение разнесённых фигур стали вариантами существующих номеров 3 и 1. Пара направлений находится в одном test_9; кластер и кластер с выбросом — в одном test_12. + +В каждом `test_N_…` находится структура теста и генератор `geometry`. `test/mod.rs` связывает номера с этими генераторами. `test/runner.rs` содержит общий протокол проверки/измерения, `test/data.rs` — представление входа, PRNG, отпечаток и преобразование координат. `config.rs` разбирает параметры, `main.rs` запускает выбранные номера. Геометрия сценариев не спрятана в общем файле generators/fixtures. + +## Запуск + +Из каталога `/Users/nailsharipov/Projects/Rust/iShape/iOverlay/performance/rust_app`: + +```bash +# Прежний способ выбора теста и размера; дополнительные опции необязательны. +./run.sh --test 0 --count 32 --complex false --multithreading false --int all + +# Парное измерение направлений одной геометрии. +./run.sh --test 9 --count 128 --orientation all --strategy all + +# Дополнительные нагрузки; один seed на весь выбранный набор. +./run.sh --test 10,11,12,13 --count 32 --seed 42 + +# Варианты существующих тестов. +./run.sh --test 1 --count 512 --layout line +./run.sh --test 3 --count 128 --spiral integer + +# Полный базовый набор. +mkdir -p results +./run.sh --label my-machine > results/baseline.csv + +# Только геометрия, независимая от sin/cos и float-адаптера. +./run.sh --test 0,1,2,3,4,5,8,9,10,11,12,13 --spiral integer > results/integer.csv + +# Серия размеров в стиле старого complex-режима, с ограничением времени. +./run.sh --test 9,10,11,12,13 --complex true --budget-seconds 30 + +# Пороговые размеры: 3992,4000,8000,16000,16008 исходных рёбер. +./run.sh --test 1 --layout line --count 499,500,1000,2000,2001 \ + --mode cold --warmup 1 --samples 3 --iterations 1 > results/thresholds.csv + +# Потоки требуют явного включения feature. +RAYON_NUM_THREADS=4 cargo run --release --locked --features multithreading -- \ + --test 1 --layout line --count 4096 --multithreading true --strategy auto,tree,frag + +cargo test --release --locked --all-features +cargo clippy --release --locked --all-targets --all-features -- -D warnings +./run.sh --list +./run.sh --help +``` + +Без параметров запускаются 0..13, count=8, seed=0, int=i32, все четыре стратегии, оба режима, без потоков. test_9 и test_12 по умолчанию выполняют оба варианта. Поэтому полный default-прогон содержит **128 строк** CSV; с `--int all` — **384**. По умолчанию test_1 использует старую grid-геометрию, test_3 — старую float-спираль. `--layout all` и `--spiral all` включают оба варианта соответствующего номера. + +`--test`, `--count`, `--strategy`, `--int`, `--mode` принимают списки через запятую или `all` там, где это указано в help. Для integer engine также поддерживаются старые обозначения 16/32/64. `--size` — алиас count, `--extended` — алиас complex=true. `--case` и legacy-feature удалены; пользуйтесь номерами. + +complex=true, как прежде, выбирает серию размеров вместо count; теперь это ограниченная базовая серия 2,4,8,16,32,64. Квадратичные тесты 0,1(grid),2,4,6,8 ограничены count<=64, более дорогой 7 — count<=16; остальные — <=4096. В complex слишком большие комбинации пропускаются с сообщением stderr; при явном count сверх лимита возвращается ошибка. Масштабные старые complex-серии не запускаются автоматически. + +## Измерение и проверка + +Общий протокол применяется к старым и новым номерам: FillRule::NonZero, Precision::HIGH, стандартные integer output options. По умолчанию 2 warm-up операции и 7 выборок по 3 операции; параметры `--warmup`, `--samples`, `--iterations` изменяют это. samples допускает 3..101. Debug-замеры запрещены. + +- **cold**: создание Overlay, загрузка готового входа, Boolean operation, освобождение результата и Overlay. +- **reuse**: восстановление исходных контуров через `reinit_with_subj_and_clip`, Boolean operation, освобождение результата; ёмкости Overlay сохраняются. + +Генерация, преобразование координат, проверка корректности, canonicalization, отпечатки и вывод находятся вне таймера. Изменённые splitter-ом сегменты не переиспользуются как исходный вход. Используется black_box. Cold обозначает создание экземпляра, а не гарантированно холодный CPU/allocator cache. + +Для исходных float-тестов 3/6/7 геометрия сохранена, а преобразование через тот же conservative FloatPointAdapter выполняется заранее с выбранным integer engine. Измеряется **integer-ядро Overlay**, без float-конвертации и float-очистки выходных контуров. Новые времена не сопоставимы напрямую со старыми опубликованными цифрами FloatOverlay. Float-входы используют sin/cos; межплатформенная побитовая стабильность не обещается, итоговая integer-геометрия может различаться между движками. Её реальный отпечаток фиксируется в CSV. Для строго стабильной спирали выбирайте `--spiral integer`. + +Перед замером каждый результат сравнивается с List-эталоном и проверяется reinit. Сравниваются ориентированные выходные границы с независимостью от порядка фигур/контуров и циклической начальной вершины. Направления контуров сохраняются, отверстия учитываются. Это строгий компаратор границ для данного корпуса; расхождения не скрываются допуском. Для test_9 проверяется точное совпадение геометрии после поворота на 90°; превосходство horizontal не предполагается. Все пересечения этой пары целочисленные. + +Мягкий `--budget-seconds` по умолчанию 30; сборка не входит в бюджет. Проверки выполняются между комбинациями, warm-up операциями и выборками; одна операция/проверка корректности не прерывается. При исчерпании бюджета незавершённая строка и остальные комбинации пропускаются, stderr сообщает причину, завершённые строки сохраняются (exit 0). Проверяйте полноту CSV. + +## Воспроизводимость и результаты + +**CSV schema=2, fixture_version=2**: добавлены поля `test` и `variant`, изменён корпус. Прежний baseline schema=1 нельзя считать baseline новой матрицы; он сохранён локально в `results/previous-layout/`. Каталог results игнорируется Git. `Cargo.lock` фиксируется для этого executable. + +CSV содержит параметры, фактическое число исходных рёбер input_edges, число контуров, bounding box, input fingerprint, число выходных контуров/рёбер, ориентированную удвоенную площадь и validated=true. Времена — целые наносекунды одной операции, усреднённые внутри выборки. Сохраняются медиана, min/max и все выборки по порядку. Для чётного количества выборок медиана округляется вниз. stdout — только CSV, сообщения — stderr. + +Метаданные включают библиотечную версию/revision/dirty, source fingerprints библиотеки и runner (включая lock), время сборки, Rust/target/profile/opt-level/debug-info/RUSTFLAGS, ОС/доступные CPU, label машины, время запуска, feature потоков и RAYON_NUM_THREADS. Git-поля могут быть unknown вне Git. Для аппаратного сравнения задавайте label с моделью CPU и закрепляйте число потоков. После изменений библиотеки запускайте через Cargo, чтобы пересобрать бинарник. + +SplitMix64 в data.rs использует собственный u64 state и wrapping-арифметику modulo 2^64: + +```text +state += 0x9e3779b97f4a7c15 +z = (state ^ (state >> 30)) * 0xbf58476d1ce4e5b9 +z = (z ^ (z >> 27)) * 0x94d049bb133111eb +return z ^ (z >> 31) +``` + +Первые пять значений seed=0: `e220a8397b1dcdaf,6e789e6aa1b965f4,06c45d188009454f,f88bb8a8724c81ec,1b39896a51a8749b`. + +test_13 генерирует 2n прямоугольников, попеременно Subject и Clip. Четыре значения на прямоугольник: x=r0%4096-2048, y=r1%4096-2048, w=8+r2%256, h=8+r3%256. Порядок вершин: (x,y),(x+w,y),(x+w,y+h),(x,y+h). test_12 использует два значения на пару: x=r0%512-256, y=r1%512-256; Subject 64×64, Clip того же размера со сдвигом (16,16); дополнительный квадрат 8×8 в (0,0) или (12000,12000). Modulo bias намеренно фиксирован. + +Input fingerprint — FNV-1a 64, offset cbf29ce484222325, prime 100000001b3, wrapping-умножение. Для Subject, затем Clip хэшируются little-endian u64: число контуров, длина каждого и пары координат i64 в двоичном дополнении. Порядок входа значим; rule/version — отдельные поля. Это диагностический хэш; StdRng/DefaultHasher не используются. + +Golden test_13 при count=8: seed=0 → 77ac91e43630115d, seed=42 → 95eba339e7c58a49, seed=u64::MAX → b3bbd963a77647f5. В cargo tests закреплены также отпечатки старых integer-генераторов, новых нагрузок и integer-спирали. При намеренном изменении входа обновляйте fixture_version и контрольные векторы. + +## Ограничения + +Каждая integer-координата проверяется до сужения типа: i16 [-16384,16383], i32 [-1073741824,1073741823], i64 [-4611686018427387904,4611686018427387903]. Не подходящие для двигателя комбинации пропускаются с сообщением stderr. Integer-fixtures не масштабируются под двигатель. + +В текущей библиотеке Auto split: List при <4000 сегментов, Tree при 4000..=16000, Frag при >16000. Запрошенный Tree также переходит в Frag при >16000. Fill Auto имеет отдельный порог <8000 после splitting; Tree/Frag используют tree-fill. В CSV strategy — запрос, split_route_hint — оценка по исходным рёбрам; нормализация, слияние и повторные проходы могут изменить сегменты. Fill route также не инструментирован. Tree/Frag на больших данных нельзя выдавать за сравнение разных split-алгоритмов. Библиотека не изменена. + +Малые smoke-данные обычно не достигают parallel-sort порога 32768; multithreading=true не доказывает фактическое распараллеливание. Test_10 содержит почти параллельные полосы с частичным перекрытием и без плотной сетки пересечений; плотные пересечения уже покрывает test_2. Строгие тесты используют размеры 1/4/8; большие или иные float-данные могут выявить новые расхождения — тогда runner сообщит ошибку. diff --git a/performance/rust_app/VALIDATION.md b/performance/rust_app/VALIDATION.md new file mode 100644 index 00000000..b3c5cb3e --- /dev/null +++ b/performance/rust_app/VALIDATION.md @@ -0,0 +1,18 @@ +# Проверка единого numbered-стенда 0.2.0 + +Проверено 12 октября 2026 года. Локальная iOverlay 9.0.1, revision `7a9e659a0355af272cb26383be169557a78e5ea4`; библиотека не изменена. Rust `1.99.0 (b940084d7 2026-09-28)`, target `aarch64-apple-darwin`, macOS. + +- Прежние файлы и номера 0..8 сохранены. Исходные integer-генераторы закреплены golden fingerprints; исходные float-генераторы сохранены с предварительной конвертацией для выбранного integer engine. +- Новые файлы: test_9_orientation, test_10_diagonal, test_11_collinear, test_12_cluster, test_13_seeded. Дублирование старых типов нагрузки удалено. Linear-layout — вариант test_1; integer-спираль — вариант test_3. +- Единственный основной бинарник принимает старые --test/--count/--complex/--int; legacy и отдельный named-case runner удалены. +- `cargo test --release --offline --locked --all-features`: 9 тестов прошли. Все номера и варианты на размерах 1/4/8, все три integer engine и четыре стратегии, reinit, аналитические площади, поворот, coordinate budget, PRNG-векторы, seeded/старые integer fingerprints проверены. +- `cargo clippy --release --offline --locked --all-targets --all-features -- -D warnings`: без предупреждений. +- `results/smoke-numbered.csv`: 384 строки — 16 вариантов номеров 0..13 × 3 engines × 4 strategies × 2 modes. count=8, seed=0, warmup=1, samples=3, iterations=1. +- `results/smoke-numbered-mt.csv`: те же 384 комбинации, включённая feature multithreading, runtime=true, RAYON_NUM_THREADS=4. Все проверки прошли; input fingerprints и сводки результатов совпали с serial-прогоном. Малые размеры не доказывают фактического parallel sort или ускорения. +- `results/baseline.csv`: новый baseline schema=2/fixture_version=2, 128 строк; default count=8, int=i32, warmup=2, samples=7, iterations=3. +- `results/thresholds.csv`: 20 строк, test_1 layout=line, 3992/4000/8000/16000/16008 исходных рёбер, i32, четыре стратегии, cold; результаты совпали. При 16008 route hint запрошенных Tree и Frag одинаков: Frag. +- `results/variants.csv`: 48 строк, test_1 layout=line и test_3 spiral=integer, count=32, все engines/strategies/modes; проверки прошли. +- CSV прочитаны Python csv.DictReader: ожидаемое число строк, schema/fixture_version, структура полей, validated=true, число выборок, медиана/min/max проверены. +- `git diff --check`: без ошибок. Все изменения ограничены performance/rust_app; examples и библиотека не изменены. Commit/push не выполнялись. + +Старые результаты/описания предыдущей организации сохранены в локальном `results/previous-layout/` и не являются baseline новой матрицы. Каталог results игнорируется Git. Команды запуска и ограничения (float, soft budget, диапазон, маршрутизация Tree→Frag) описаны в README. diff --git a/performance/rust_app/build.rs b/performance/rust_app/build.rs new file mode 100644 index 00000000..6af87985 --- /dev/null +++ b/performance/rust_app/build.rs @@ -0,0 +1,166 @@ +use std::{ + env, fs, + path::Path, + process::Command, + time::{SystemTime, UNIX_EPOCH}, +}; + +fn command(program: &str, args: &[&str]) -> String { + Command::new(program) + .args(args) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|| "unknown".into()) +} +fn hash_files(root: &Path, hash: &mut u64) { + let mut paths: Vec<_> = fs::read_dir(root) + .unwrap() + .map(|e| e.unwrap().path()) + .collect(); + paths.sort(); + for path in paths { + if path.is_dir() { + hash_files(&path, hash); + } else if path.extension().is_some_and(|e| e == "rs") + || path + .file_name() + .is_some_and(|n| n == "Cargo.toml" || n == "Cargo.lock") + { + // Include relative traversal names and delimiters as well as contents. + for byte in path + .file_name() + .unwrap() + .to_string_lossy() + .bytes() + .chain([0]) + .chain(fs::read(&path).unwrap()) + .chain([0]) + { + *hash = (*hash ^ u64::from(byte)).wrapping_mul(0x100000001b3); + } + } + } +} +fn emit(key: &str, value: impl std::fmt::Display) { + println!("cargo:rustc-env={key}={value}"); +} +fn main() { + let manifest = env::var("CARGO_MANIFEST_DIR").unwrap(); + let root = Path::new(&manifest); + let lib = root.join("../../iOverlay").canonicalize().unwrap(); + for path in [ + root.join("src"), + root.join("Cargo.toml"), + root.join("Cargo.lock"), + root.join("build.rs"), + lib.join("src"), + lib.join("Cargo.toml"), + ] { + println!("cargo:rerun-if-changed={}", path.display()); + } + for git_path in ["HEAD", "index", "packed-refs"] { + let path = command( + "git", + &[ + "-C", + lib.to_str().unwrap(), + "rev-parse", + "--path-format=absolute", + "--git-path", + git_path, + ], + ); + if path != "unknown" { + println!("cargo:rerun-if-changed={path}"); + } + } + let branch_ref = command( + "git", + &["-C", lib.to_str().unwrap(), "symbolic-ref", "-q", "HEAD"], + ); + if branch_ref != "unknown" { + let path = command( + "git", + &[ + "-C", + lib.to_str().unwrap(), + "rev-parse", + "--path-format=absolute", + "--git-path", + &branch_ref, + ], + ); + if path != "unknown" { + println!("cargo:rerun-if-changed={path}"); + } + } + let text = fs::read_to_string(lib.join("Cargo.toml")).unwrap(); + let version = text + .lines() + .find_map(|s| s.strip_prefix("version = ")) + .unwrap() + .trim_matches('"'); + emit("BENCH_LIBRARY_VERSION", version); + let mut hash = 0xcbf29ce484222325; + hash_files(&lib.join("src"), &mut hash); + for byte in text.bytes() { + hash = (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3); + } + emit("BENCH_LIBRARY_SOURCE", format!("{hash:016x}")); + let mut hash = 0xcbf29ce484222325; + hash_files(&root.join("src"), &mut hash); + for name in ["Cargo.toml", "Cargo.lock", "build.rs"] { + for byte in fs::read(root.join(name)).unwrap() { + hash = (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3); + } + } + emit("BENCH_RUNNER_SOURCE", format!("{hash:016x}")); + emit( + "BENCH_RUSTC", + command(&env::var("RUSTC").unwrap(), &["--version"]), + ); + emit("BENCH_TARGET", env::var("TARGET").unwrap()); + emit("BENCH_PROFILE", env::var("PROFILE").unwrap()); + emit("BENCH_OPT_LEVEL", env::var("OPT_LEVEL").unwrap()); + emit("BENCH_DEBUG", env::var("DEBUG").unwrap()); + emit( + "BENCH_RUSTFLAGS", + env::var("CARGO_ENCODED_RUSTFLAGS") + .unwrap_or_default() + .replace('\x1f', " "), + ); + emit( + "BENCH_BUILD_UNIX", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + ); + emit( + "BENCH_LIBRARY_REV", + command("git", &["-C", lib.to_str().unwrap(), "rev-parse", "HEAD"]), + ); + let status = command( + "git", + &[ + "-C", + lib.to_str().unwrap(), + "status", + "--porcelain", + "--", + ".", + ], + ); + emit( + "BENCH_LIBRARY_DIRTY", + if status == "unknown" { + "unknown" + } else if status.is_empty() { + "false" + } else { + "true" + }, + ); +} diff --git a/performance/rust_app/run.sh b/performance/rust_app/run.sh index 1969e792..8be59369 100755 --- a/performance/rust_app/run.sh +++ b/performance/rust_app/run.sh @@ -1,3 +1,4 @@ #!/bin/bash - -target/release/rust_app --multithreading false --complex true --test 0 --int all +set -euo pipefail +cd "$(dirname "$0")" +cargo run --release --locked --bin rust_app -- "$@" diff --git a/performance/rust_app/src/config.rs b/performance/rust_app/src/config.rs new file mode 100644 index 00000000..1da5d5f5 --- /dev/null +++ b/performance/rust_app/src/config.rs @@ -0,0 +1,308 @@ +use std::{collections::BTreeMap, time::Duration}; + +#[derive(Debug)] +pub struct Config { + pub tests: Vec, + pub counts: Vec, + pub seed: u64, + pub strategies: Vec, + pub engines: Vec, + pub modes: Vec, + pub multithreading: bool, + pub label: String, + pub warmup: usize, + pub samples: usize, + pub iterations: usize, + pub budget: Duration, + pub complex: bool, + pub orientation: String, + pub outlier: String, + pub spiral: String, + pub layout: String, +} +impl Config { + pub fn variants(&self, id: usize) -> Vec<&str> { + let (value, all): (&str, &[&str]) = match id { + 1 => (&self.layout, &["grid", "line"]), + 3 => (&self.spiral, &["float", "integer"]), + 9 => (&self.orientation, &["horizontal", "vertical"]), + 12 => (&self.outlier, &["cluster", "outlier"]), + _ => return vec!["default"], + }; + if value == "all" { + all.to_vec() + } else { + vec![value] + } + } +} +const HELP: &str = "iOverlay numbered performance tests (release)\n\ +--test 0..13|all (comma lists; default all) --count 8 (comma lists)\n\ +--complex false|true (true uses 2,4,8,16,32,64, caps quadratic tests)\n\ +--strategy auto|list|tree|frag|all (default all) --int i16|i32|i64|all (default i32)\n\ +--mode cold|reuse|all --multithreading false|true (requires feature multithreading)\n\ +--seed 0 --warmup 2 --samples 7 --iterations 3 --budget-seconds 30 --label local\n\ +--orientation horizontal|vertical|all (test 9; default all)\n\ +--outlier false|true|all (test 12; default all)\n\ +--spiral float|integer|all (test 3; default float)\n\ +--layout grid|line|all (test 1; default grid)\n\ +--list lists numbered tests. stdout is CSV; diagnostics are on stderr.\n"; + +pub fn parse(args: impl Iterator) -> Result, String> { + let mut args = args.peekable(); + let mut map = BTreeMap::new(); + while let Some(arg) = args.next() { + if arg == "--help" || arg == "-h" { + eprint!("{HELP}"); + return Ok(None); + } + if arg == "--list" { + for (id, name) in crate::test::NAMES.iter().enumerate() { + eprintln!("test_{id}_{name}"); + } + return Ok(None); + } + if arg == "--extended" { + map.insert("complex".to_string(), "true".to_string()); + continue; + } + let key = arg + .strip_prefix("--") + .or_else(|| arg.strip_prefix('-')) + .ok_or_else(|| format!("expected --option: {arg}"))?; + let key = if key == "size" { "count" } else { key }; + if ![ + "test", + "count", + "complex", + "seed", + "strategy", + "int", + "mode", + "multithreading", + "warmup", + "samples", + "iterations", + "budget-seconds", + "label", + "orientation", + "outlier", + "spiral", + "layout", + ] + .contains(&key) + { + return Err(format!("unknown option: {arg}")); + } + let value = if ["complex", "multithreading"].contains(&key) + && args.peek().is_none_or(|s| s.starts_with('-')) + { + "true".into() + } else { + args.next() + .filter(|s| !s.starts_with('-')) + .ok_or_else(|| format!("missing value for {arg}"))? + }; + if map.insert(key.to_string(), value).is_some() { + return Err(format!("duplicate option: {arg}")); + } + } + let get = |key: &str, default: &str| map.get(key).cloned().unwrap_or_else(|| default.into()); + let choices = |key: &str, default: &str, allowed: &[&str]| -> Result, String> { + let value = get(key, default); + let result: Vec = if value == "all" { + allowed.iter().map(|s| s.to_string()).collect() + } else { + value.split(',').map(str::to_string).collect() + }; + if result.iter().any(|s| !allowed.contains(&s.as_str())) { + return Err(format!("invalid {key}: {value}")); + } + let mut unique = result.clone(); + unique.sort(); + unique.dedup(); + if unique.len() != result.len() { + return Err(format!("duplicate {key} values")); + } + Ok(result) + }; + let number = |key: &str, default: &str, min: usize, max: usize| -> Result { + let n = get(key, default) + .parse::() + .map_err(|_| format!("invalid {key}"))?; + if !(min..=max).contains(&n) { + return Err(format!("{key} must be in {min}..={max}")); + } + Ok(n) + }; + let complex = get("complex", "false") + .parse::() + .map_err(|_| "invalid complex boolean".to_string())?; + let tests = if get("test", "all") == "all" { + (0..crate::test::NAMES.len()).collect() + } else { + get("test", "all") + .split(',') + .map(|s| { + s.parse::() + .map_err(|_| "invalid test number".to_string()) + }) + .collect::, _>>()? + }; + if tests.iter().any(|&id| id >= crate::test::NAMES.len()) { + return Err("test must be in 0..=13".into()); + } + let mut unique = tests.clone(); + unique.sort(); + unique.dedup(); + if unique.len() != tests.len() { + return Err("duplicate test numbers".into()); + } + let counts = if complex { + vec![2, 4, 8, 16, 32, 64] + } else { + get("count", "8") + .split(',') + .map(|s| s.parse::().map_err(|_| "invalid count".to_string())) + .collect::, _>>()? + }; + if counts.iter().any(|n| !(1..=4096).contains(n)) { + return Err("count must be in 1..=4096".into()); + } + let seed = get("seed", "0"); + let seed = if let Some(s) = seed.strip_prefix("0x") { + u64::from_str_radix(s, 16) + } else { + seed.parse() + } + .map_err(|_| "invalid seed".to_string())?; + let multithreading = get("multithreading", "false") + .parse::() + .map_err(|_| "invalid multithreading boolean".to_string())?; + if multithreading && !cfg!(feature = "multithreading") { + return Err("multithreading=true requires --features multithreading".into()); + } + let mut engines = choices("int", "i32", &["i16", "i32", "i64", "16", "32", "64"])?; + if get("int", "i32") == "all" { + engines = vec!["i16".into(), "i32".into(), "i64".into()]; + } + for e in &mut engines { + if !e.starts_with('i') { + *e = format!("i{e}"); + } + } + let mut engine_unique = engines.clone(); + engine_unique.sort(); + engine_unique.dedup(); + if engine_unique.len() != engines.len() { + return Err("duplicate integer engines".into()); + } + let single = |key: &str, default: &str, allowed: &[&str]| -> Result { + let v = get(key, default); + if v != "all" && !allowed.contains(&v.as_str()) { + return Err(format!("invalid {key}: {v}")); + } + Ok(v) + }; + let outlier = single("outlier", "all", &["false", "true"])?; + let outlier = match outlier.as_str() { + "false" => "cluster".into(), + "true" => "outlier".into(), + _ => outlier, + }; + let config = Config { + tests, + counts, + seed, + strategies: choices("strategy", "all", &["auto", "list", "tree", "frag"])?, + engines, + modes: choices("mode", "all", &["cold", "reuse"])?, + multithreading, + label: get("label", "local"), + warmup: number("warmup", "2", 1, 100)?, + samples: number("samples", "7", 3, 101)?, + iterations: number("iterations", "3", 1, 10000)?, + budget: Duration::from_secs(number("budget-seconds", "30", 1, 3600)? as u64), + complex, + orientation: single("orientation", "all", &["horizontal", "vertical"])?, + outlier, + spiral: single("spiral", "float", &["float", "integer"])?, + layout: single("layout", "grid", &["grid", "line"])?, + }; + if !complex { + for &id in &config.tests { + for variant in config.variants(id) { + for &n in &config.counts { + if n > crate::test::max_count(id, variant) { + return Err(format!( + "test {id} {variant}: count exceeds {}", + crate::test::max_count(id, variant) + )); + } + } + } + } + } + Ok(Some(config)) +} +#[cfg(test)] +mod checks { + use super::*; + fn p(args: &[&str]) -> Result, String> { + parse(args.iter().map(|s| s.to_string())) + } + #[test] + fn original_numeric_cli() { + let c = p(&[ + "--test", + "0", + "--count", + "32", + "--complex", + "false", + "--multithreading", + "false", + "--int", + "all", + ]) + .unwrap() + .unwrap(); + assert_eq!(c.tests, vec![0]); + assert_eq!(c.counts, vec![32]); + assert_eq!(c.engines, vec!["i16", "i32", "i64"]); + assert_eq!( + p(&["--test", "3"]).unwrap().unwrap().variants(3), + vec!["float"] + ); + assert_eq!( + p(&["--test", "1"]).unwrap().unwrap().variants(1), + vec!["grid"] + ); + assert_eq!( + p(&["-test", "9", "-count", "8", "-int", "32"]) + .unwrap() + .unwrap() + .engines, + vec!["i32"] + ); + } + #[test] + fn limits_and_variants() { + assert!(p(&["--test", "14"]).is_err()); + assert!(p(&["--test"]).is_err()); + assert!(p(&["--count", "0"]).is_err()); + assert!(p(&["--samples", "2"]).is_err()); + assert!(p(&["--test", "2", "--count", "65"]).is_err()); + assert_eq!( + p(&["--test", "9"]).unwrap().unwrap().variants(9), + vec!["horizontal", "vertical"] + ); + assert_eq!( + p(&["--test", "12", "--outlier", "true"]) + .unwrap() + .unwrap() + .variants(12), + vec!["outlier"] + ); + } +} diff --git a/performance/rust_app/src/main.rs b/performance/rust_app/src/main.rs index eccd745f..e84c1930 100644 --- a/performance/rust_app/src/main.rs +++ b/performance/rust_app/src/main.rs @@ -1,276 +1,73 @@ -use crate::test::test_0_checkerboard::CheckerboardTest; -use crate::test::test_1_not_overlap::NotOverlapTest; -use crate::test::test_2_lines_net::LinesNetTest; -use crate::test::test_3_spiral::SpiralTest; -use crate::test::test_4_windows::WindowsTest; -use crate::test::test_5_nested_squares::CrossTest; -use crate::test::test_6_corrosion::CorrosionTest; -use crate::test::test_7_concentric::ConcentricTest; -use crate::test::test_8_wind_mill::WindMillTest; -use i_overlay::core::overlay_rule::OverlayRule; -use i_overlay::core::solver::{MultithreadOptions, Precision, Solver, Strategy}; -use std::collections::HashMap; -use std::env; - +mod config; mod test; - -#[derive(Clone, Copy)] -enum IntEngine { - I16, - I32, - I64, -} - -impl IntEngine { - fn parse(value: &str) -> Vec { - match value { - "i16" | "16" => vec![Self::I16], - "i32" | "32" => vec![Self::I32], - "i64" | "64" => vec![Self::I64], - "all" => vec![Self::I16, Self::I32, Self::I64], - _ => panic!("Unknown int engine: {value}. Use i16, i32, i64, or all"), - } - } - - fn label(self) -> &'static str { - match self { - Self::I16 => "i16", - Self::I32 => "i32", - Self::I64 => "i64", - } - } -} - -fn main() { - let args = env::args(); - let mut args_iter = args.peekable(); - let mut args_map = HashMap::new(); - - while let Some(arg) = args_iter.next() { - if let Some(key) = arg.strip_prefix("--").or_else(|| arg.strip_prefix('-')) { - // If the next argument is also a key, store a boolean flag; otherwise, store the value. - let value = if args_iter.peek().map_or(false, |a| a.starts_with('-')) { - "true".to_string() - } else { - args_iter.next().unwrap() - }; - args_map.insert(key.to_owned(), value); - } - } - - #[cfg(debug_assertions)] - { - if args_map.is_empty() { - args_map.insert("multithreading".to_string(), "false".to_string()); - args_map.insert("complex".to_string(), "false".to_string()); - args_map.insert("test".to_string(), 3.to_string()); - args_map.insert("int".to_string(), "i64".to_string()); - let count = 4096; - args_map.insert("count".to_string(), count.to_string()); - } - } - - let test_key = args_map.get("test").expect("Test number is not set"); - let multithreading_key = args_map - .get("multithreading") - .expect("Multithreading is not set"); - let complex_key = args_map.get("complex").expect("Complex is not set"); - let int_key = args_map.get("int").map(String::as_str).unwrap_or("i32"); - - let test: usize = test_key - .parse() - .expect("Unable to parse test as an integer"); - let multithreading: bool = multithreading_key - .parse() - .expect("Unable to parse multithreading as an boolean"); - let complex: bool = complex_key - .parse() - .expect("Unable to parse complex as an boolean"); - let int_engines = IntEngine::parse(int_key); - - let multithreading = if multithreading { - Some(MultithreadOptions::default()) - } else { - None - }; - - let solver = Solver { - strategy: Strategy::Auto, - precision: Precision::HIGH, - multithreading, - }; - - for int_engine in int_engines { - println!("int engine: {}", int_engine.label()); - run_selected_test(int_engine, test, complex, &args_map, solver); - } -} - -fn run_selected_test( - int_engine: IntEngine, - test: usize, - complex: bool, - args_map: &HashMap, - solver: Solver, -) { - match int_engine { - IntEngine::I16 => run_selected_test_with_int::(test, complex, args_map, solver), - IntEngine::I32 => run_selected_test_with_int::(test, complex, args_map, solver), - IntEngine::I64 => run_selected_test_with_int::(test, complex, args_map, solver), - } -} - -fn run_selected_test_with_int( - test: usize, - complex: bool, - args_map: &HashMap, - solver: Solver, -) { - if complex { - match test { - 0 => { - run_test_0::(solver); - } - 1 => { - run_test_1::(solver); - } - 2 => { - run_test_2::(solver); - } - 3 => { - run_test_3::(solver); - } - 4 => { - run_test_4::(solver); - } - 5 => { - run_test_5::(solver); - } - 6 => { - run_test_6::(solver); - } - 7 => { - run_test_7::(solver); - } - 8 => { - run_test_8::(solver); - } - _ => { - println!("Test is not found"); - } - } - } else { - let count_key = args_map.get("count").expect("Count is not set"); - let count: usize = count_key - .parse() - .expect("Unable to parse count as an integer"); - match test { - 0 => { - CheckerboardTest::run::(count, OverlayRule::Xor, solver, 1.0); - } - 1 => { - NotOverlapTest::run::(count, OverlayRule::Union, solver, 1.0); - } - 2 => { - LinesNetTest::run::(count, OverlayRule::Intersect, solver, 1.0); - } - 3 => { - SpiralTest::run::(count, solver, 100.0); - } - 4 => { - WindowsTest::run::(count, OverlayRule::Difference, solver, 1.0); - } - 5 => { - CrossTest::run::(count, OverlayRule::Xor, solver, 1.0); - } - 6 => { - CorrosionTest::run::(count, OverlayRule::Difference, solver, 1.0); - } - 7 => { - ConcentricTest::run::(count, OverlayRule::Intersect, solver, 1.0); - } - 8 => { - WindMillTest::run::(count, OverlayRule::Intersect, solver, 1.0); - } - _ => { - println!("Test is not found"); +use config::Config; +use std::{ + io::{self, Write}, + time::Instant, +}; + +fn execute(config: Config) -> Result<(), String> { + if cfg!(debug_assertions) { + return Err("use cargo run --release".into()); + } + let deadline = Instant::now() + config.budget; + let stdout = io::stdout(); + let mut out = io::BufWriter::new(stdout.lock()); + writeln!(out, "{}", test::runner::HEADER).map_err(|e| e.to_string())?; + for &id in &config.tests { + for variant in config.variants(id) { + for &n in &config.counts { + if n > test::max_count(id, variant) { + if config.complex { + eprintln!("skip test {id} {variant} count={n}: size cap"); + continue; + } + return Err(format!( + "test {id} {variant}: count exceeds {}", + test::max_count(id, variant) + )); + } + for engine in &config.engines { + if Instant::now() >= deadline { + eprintln!("budget reached; remaining combinations omitted"); + return Ok(()); + } + let completed = match engine.as_str() { + "i16" => { + let f = test::generate::(id, n, config.seed, variant)?; + test::runner::run::( + &config, id, variant, n, &f, deadline, &mut out, + )? + } + "i32" => { + let f = test::generate::(id, n, config.seed, variant)?; + test::runner::run::( + &config, id, variant, n, &f, deadline, &mut out, + )? + } + "i64" => { + let f = test::generate::(id, n, config.seed, variant)?; + test::runner::run::( + &config, id, variant, n, &f, deadline, &mut out, + )? + } + _ => unreachable!(), + }; + if !completed { + eprintln!("budget reached; incomplete row omitted"); + return Ok(()); + } + } } } } + out.flush().map_err(|e| e.to_string()) } - -fn run_test_0(solver: Solver) { - println!("run Checkerboard test"); - for i in 1..11 { - let n = 1 << i; - CheckerboardTest::run::(n, OverlayRule::Xor, solver, 1000.0); - } -} - -fn run_test_1(solver: Solver) { - println!("run NotOverlap test"); - for i in 1..11 { - let n = 1 << i; - NotOverlapTest::run::(n, OverlayRule::Xor, solver, 1000.0); - } -} - -fn run_test_2(solver: Solver) { - println!("run LinesNet test"); - for i in 1..11 { - let n = 1 << i; - LinesNetTest::run::(n, OverlayRule::Intersect, solver, 500.0); - } -} - -fn run_test_3(solver: Solver) { - println!("run Spiral test"); - for i in 1..20 { - let n = 1 << i; - SpiralTest::run::(n, solver, 1000.0) - } -} - -fn run_test_4(solver: Solver) { - println!("run Windows test"); - for i in 1..11 { - let n = 1 << i; - WindowsTest::run::(n, OverlayRule::Difference, solver, 500.0); - } -} - -fn run_test_5(solver: Solver) { - println!("run NestedSquares test"); - for i in 1..17 { - let n = 1 << i; - CrossTest::run::(n, OverlayRule::Xor, solver, 500.0); - } -} - -fn run_test_6(solver: Solver) { - println!("run Corrosion test"); - let mut n = 1; - for _ in 1..11 { - CorrosionTest::run::(n, OverlayRule::Difference, solver, 100.0); - n = n << 1; - } -} - -fn run_test_7(solver: Solver) { - println!("run Concentric test"); - let mut n = 1; - for _ in 1..11 { - ConcentricTest::run::(n, OverlayRule::Intersect, solver, 100.0); - n = n << 1; - } -} - -fn run_test_8(solver: Solver) { - println!("run WindMill test"); - let mut n = 1; - for _ in 1..11 { - WindMillTest::run::(n, OverlayRule::Difference, solver, 100.0); - n = n << 1; +fn main() { + let result = + config::parse(std::env::args().skip(1)).and_then(|c| c.map(execute).unwrap_or(Ok(()))); + if let Err(e) = result { + eprintln!("error: {e}"); + std::process::exit(2); } - WindMillTest::validate::(100, OverlayRule::Difference, solver); } diff --git a/performance/rust_app/src/test/data.rs b/performance/rust_app/src/test/data.rs new file mode 100644 index 00000000..c1dd8b9e --- /dev/null +++ b/performance/rust_app/src/test/data.rs @@ -0,0 +1,135 @@ +use super::runner::Engine; +use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::i_shape::int::path::IntPaths; +pub type Point = (i64, i64); +pub type Path = Vec; + +#[derive(Clone, Debug)] +pub struct Fixture { + pub subj: Vec, + pub clip: Vec, + pub rule: OverlayRule, +} + +pub fn rect(x: i64, y: i64, w: i64, h: i64) -> Path { + vec![(x, y), (x + w, y), (x + w, y + h), (x, y + h)] +} + +pub fn rotate(paths: &mut [Path]) { + for path in paths { + for (x, y) in path { + (*x, *y) = (-*y, *x); + } + } +} + +impl Fixture { + pub fn edges(&self) -> usize { + self.subj.iter().chain(&self.clip).map(Vec::len).sum() + } + + pub fn bounds(&self) -> (i64, i64, i64, i64) { + let mut b = (i64::MAX, i64::MAX, i64::MIN, i64::MIN); + for &(x, y) in self.subj.iter().chain(&self.clip).flatten() { + b = (b.0.min(x), b.1.min(y), b.2.max(x), b.3.max(y)); + } + b + } + + // FNV-1a over domain-separated little-endian u64 lengths and i64 coordinates. + // Contour order is intentionally significant for the INPUT fingerprint. + pub fn fingerprint(&self) -> u64 { + let mut hash = 0xcbf29ce484222325u64; + for paths in [&self.subj, &self.clip] { + hash_word(&mut hash, paths.len() as u64); + for path in paths { + hash_word(&mut hash, path.len() as u64); + for &(x, y) in path { + hash_word(&mut hash, x as u64); + hash_word(&mut hash, y as u64); + } + } + } + hash + } +} + +fn hash_word(hash: &mut u64, word: u64) { + for byte in word.to_le_bytes() { + *hash = (*hash ^ u64::from(byte)).wrapping_mul(0x100000001b3); + } +} + +/// SplitMix64, including zero seed. All arithmetic explicitly wraps modulo 2^64. +pub struct Prng(pub u64); +impl Prng { + pub fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9e3779b97f4a7c15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); + z ^ (z >> 31) + } +} + +impl Fixture { + pub fn from_int(subj: IntPaths, clip: IntPaths, rule: OverlayRule) -> Self { + let cvt = |paths: IntPaths| { + paths + .into_iter() + .map(|p| p.into_iter().map(|p| (p.x, p.y)).collect()) + .collect() + }; + Self { + subj: cvt(subj), + clip: cvt(clip), + rule, + } + } + pub fn from_float( + subj: Vec>, + clip: Vec>, + rule: OverlayRule, + ) -> Self { + use i_overlay::i_float::adapter::FloatPointAdapter; + let adapter = FloatPointAdapter::<[f64; 2], I>::with_iter_conservative( + subj.iter().chain(&clip).flatten(), + ); + let cvt = |paths: Vec>| { + paths + .into_iter() + .map(|p| { + p.iter() + .map(|p| { + let q = adapter.float_to_int(p); + (q.x.to_i64(), q.y.to_i64()) + }) + .collect() + }) + .collect() + }; + Self { + subj: cvt(subj), + clip: cvt(clip), + rule, + } + } +} +#[cfg(test)] +mod checks { + use super::*; + #[test] + fn splitmix64_reference_vector() { + let mut r = Prng(0); + assert_eq!( + (0..5).map(|_| r.next()).collect::>(), + vec![ + 0xe220a8397b1dcdaf, + 0x6e789e6aa1b965f4, + 0x06c45d188009454f, + 0xf88bb8a8724c81ec, + 0x1b39896a51a8749b + ] + ); + } +} diff --git a/performance/rust_app/src/test/mod.rs b/performance/rust_app/src/test/mod.rs index bdac2a59..9454b69f 100644 --- a/performance/rust_app/src/test/mod.rs +++ b/performance/rust_app/src/test/mod.rs @@ -1,4 +1,10 @@ +pub(crate) mod data; +pub(crate) mod runner; pub(crate) mod test_0_checkerboard; +pub(crate) mod test_10_diagonal; +pub(crate) mod test_11_collinear; +pub(crate) mod test_12_cluster; +pub(crate) mod test_13_seeded; pub(crate) mod test_1_not_overlap; pub(crate) mod test_2_lines_net; pub(crate) mod test_3_spiral; @@ -7,4 +13,239 @@ pub(crate) mod test_5_nested_squares; pub(crate) mod test_6_corrosion; pub(crate) mod test_7_concentric; pub(crate) mod test_8_wind_mill; +pub(crate) mod test_9_orientation; pub(crate) mod util; + +pub const NAMES: &[&str] = &[ + "checkerboard", + "not_overlap", + "lines_net", + "spiral", + "windows", + "nested_squares", + "corrosion", + "concentric", + "wind_mill", + "orientation", + "diagonal", + "collinear", + "cluster", + "seeded", +]; + +pub fn max_count(id: usize, variant: &str) -> usize { + match id { + 0 | 2 | 4 | 6 | 8 => 64, + 1 if variant == "grid" => 64, + 7 => 16, + _ => 4096, + } +} +pub fn generate( + id: usize, + n: usize, + seed: u64, + variant: &str, +) -> Result { + if id >= NAMES.len() { + return Err(format!("unknown test: {id}")); + } + if n == 0 || n > max_count(id, variant) { + return Err(format!( + "test {id} {variant}: count must be in 1..={}", + max_count(id, variant) + )); + } + Ok(match id { + 0 => test_0_checkerboard::CheckerboardTest::geometry(n), + 1 => test_1_not_overlap::NotOverlapTest::geometry(n, variant == "line"), + 2 => test_2_lines_net::LinesNetTest::geometry(n), + 3 => test_3_spiral::SpiralTest::geometry::(n, variant == "integer"), + 4 => test_4_windows::WindowsTest::geometry(n), + 5 => test_5_nested_squares::CrossTest::geometry(n), + 6 => test_6_corrosion::CorrosionTest::geometry::(n), + 7 => test_7_concentric::ConcentricTest::geometry::(n), + 8 => test_8_wind_mill::WindMillTest::geometry(n), + 9 => test_9_orientation::OrientationTest::geometry(n, variant == "vertical"), + 10 => test_10_diagonal::DiagonalTest::geometry(n), + 11 => test_11_collinear::CollinearTest::geometry(n), + 12 => test_12_cluster::ClusterTest::geometry(n, seed, variant == "outlier"), + 13 => test_13_seeded::SeededTest::geometry(n, seed), + _ => unreachable!(), + }) +} + +#[cfg(test)] +mod checks { + use super::*; + use i_overlay::core::{fill_rule::FillRule, overlay::Overlay}; + use runner::{area2, canonical, convert, normalize, solver, Engine}; + + fn check() { + for id in 0..NAMES.len() { + let variants: &[&str] = match id { + 1 => &["grid", "line"], + 3 => &["float", "integer"], + 9 => &["horizontal", "vertical"], + 12 => &["cluster", "outlier"], + _ => &["default"], + }; + for variant in variants { + for n in [1, 4, 8] { + let f = generate::(id, n, 0, variant).unwrap(); + let (subj, clip) = convert::(&f).unwrap(); + let expected = canonical( + &Overlay::from_subj_and_clip_custom( + &subj, + &clip, + Default::default(), + solver("list", false), + ) + .overlay(f.rule, FillRule::NonZero), + ); + for strategy in ["auto", "tree", "frag"] { + let mut overlay = Overlay::from_subj_and_clip_custom( + &subj, + &clip, + Default::default(), + solver(strategy, false), + ); + assert_eq!( + canonical(&overlay.overlay(f.rule, FillRule::NonZero)), + expected, + "test {id} {variant} n={n} {strategy} i{}", + I::BITS + ); + overlay.reinit_with_subj_and_clip(&subj, &clip); + assert_eq!( + canonical(&overlay.overlay(f.rule, FillRule::NonZero)), + expected + ); + } + let area = match id { + 1 if *variant == "line" => Some(2 * n as i128 * 64), + 1 => Some((n * n + (n - 1) * (n - 1)) as i128 * 100), + 2 => Some(50 * n as i128 * (2 * n as i128 - 1)), + 4 => Some(n as i128 * n as i128 * 300), + 9 => Some(n as i128 * (96 * n as i128 + 24)), + _ => None, + }; + if let Some(a) = area { + assert_eq!(area2(&expected), 2 * a, "test {id} {variant}"); + } + if id == 8 { + assert_eq!(expected.len(), 4 * n * n); + } + if id == 9 { + let v = generate::( + 9, + n, + 0, + if *variant == "horizontal" { + "vertical" + } else { + "horizontal" + }, + ) + .unwrap(); + let (vs, vc) = convert::(&v).unwrap(); + let actual = canonical( + &Overlay::from_subj_and_clip_custom( + &vs, + &vc, + Default::default(), + solver("auto", false), + ) + .overlay(v.rule, FillRule::NonZero), + ); + let mut turned = expected.clone(); + data::rotate(&mut turned); + if *variant == "vertical" { + data::rotate(&mut turned); + data::rotate(&mut turned); + } + turned = turned.into_iter().map(normalize).collect(); + turned.sort(); + assert_eq!(turned, actual); + } + } + } + } + } + #[test] + fn geometry_i16() { + check::(); + } + #[test] + fn geometry_i32() { + check::(); + } + #[test] + fn geometry_i64() { + check::(); + } + #[test] + fn preserved_integer_geometry_fingerprints() { + for (id, variant, h) in [ + (0, "default", 0xa5ecab9f1142e330), + (1, "grid", 0xc4788e07a48e5030), + (2, "default", 0x1a29e4309422f995), + (4, "default", 0x7d79add12de90845), + (5, "default", 0xf9e56b15fab43ea5), + (8, "default", 0xa296ee90b1d68f8d), + ] { + for actual in [ + generate::(id, 8, 0, variant).unwrap(), + generate::(id, 8, 0, variant).unwrap(), + generate::(id, 8, 0, variant).unwrap(), + ] { + assert_eq!(actual.fingerprint(), h, "test {id}"); + } + } + for (id, variant, h) in [ + (1, "line", 0x5bc125b5c077b125), + (3, "integer", 0x15c955bcde8dd41b), + (9, "horizontal", 0x2e8379b513bb23a5), + (9, "vertical", 0x2e6025fd4dd7ea65), + (10, "default", 0xf5d2281a93ecb815), + (11, "default", 0x83e26e355c378485), + (12, "cluster", 0x4db2dda094b2b7a0), + (12, "outlier", 0x61db173897cc7820), + (13, "default", 0xbd5ec9746049328d), + ] { + assert_eq!( + generate::(id, 32, 0, variant).unwrap().fingerprint(), + h + ); + } + } + + #[test] + fn seeded_vectors_and_pair_structure() { + for (seed, h) in [ + (0, 0x77ac91e43630115d), + (42, 0x95eba339e7c58a49), + (u64::MAX, 0xb3bbd963a77647f5), + ] { + assert_eq!( + generate::(13, 8, seed, "default") + .unwrap() + .fingerprint(), + h + ); + } + let mut h = generate::(9, 32, 0, "horizontal").unwrap(); + data::rotate(&mut h.subj); + data::rotate(&mut h.clip); + let v = generate::(9, 32, 0, "vertical").unwrap(); + assert_eq!(h.subj, v.subj); + assert_eq!(h.clip, v.clip); + let a = generate::(12, 32, 0, "cluster").unwrap(); + let b = generate::(12, 32, 0, "outlier").unwrap(); + assert_eq!(a.edges(), b.edges()); + assert_eq!(&a.subj[..32], &b.subj[..32]); + assert_eq!(a.clip, b.clip); + assert!(generate::(2, 65, 0, "default").is_err()); + assert!(generate::(14, 8, 0, "default").is_err()); + } +} diff --git a/performance/rust_app/src/test/runner.rs b/performance/rust_app/src/test/runner.rs new file mode 100644 index 00000000..be3c98c2 --- /dev/null +++ b/performance/rust_app/src/test/runner.rs @@ -0,0 +1,345 @@ +use super::data::{self, Fixture, Path}; +use crate::config::Config; +use i_overlay::core::{ + fill_rule::FillRule, + integer::OverlayInt, + overlay::Overlay, + solver::{MultithreadOptions, Precision, Solver, Strategy}, +}; +use i_overlay::i_float::int::point::IntPoint; +use i_overlay::i_shape::int::{path::IntPaths, shape::IntShapes}; +use std::{ + hint::black_box, + io::{self, Write}, + time::Instant, +}; +pub trait Engine: OverlayInt { + fn from_i64(n: i64) -> Self; + fn to_i64(self) -> i64; +} +macro_rules! engine { + ($i:ty) => { + impl Engine for $i { + fn from_i64(n: i64) -> Self { + n as Self + } + fn to_i64(self) -> i64 { + self as i64 + } + } + }; +} +engine!(i16); +engine!(i32); +engine!(i64); + +pub fn solver(strategy: &str, multithreading: bool) -> Solver { + Solver { + strategy: match strategy { + "auto" => Strategy::Auto, + "list" => Strategy::List, + "tree" => Strategy::Tree, + "frag" => Strategy::Frag, + _ => unreachable!(), + }, + precision: Precision::HIGH, + multithreading: multithreading.then(MultithreadOptions::default), + } +} + +pub fn convert(f: &Fixture) -> Result<(IntPaths, IntPaths), String> { + let limit = 1i128 << (I::BITS - 2); + let b = f.bounds(); + if [b.0, b.1].iter().any(|&v| i128::from(v) < -limit) + || [b.2, b.3].iter().any(|&v| i128::from(v) >= limit) + { + return Err(format!( + "coordinates {:?} exceed i{} integer overlay range [-{},{}]", + b, + I::BITS, + limit, + limit - 1 + )); + } + let cvt = |paths: &[Path]| { + paths + .iter() + .map(|p| { + p.iter() + .map(|&(x, y)| IntPoint::new(I::from_i64(x), I::from_i64(y))) + .collect() + }) + .collect() + }; + Ok((cvt(&f.subj), cvt(&f.clip))) +} + +// Keep winding, remove cyclic-start and contour/shape ordering. Default output +// simplifies collinear vertices. Grouping is unnecessary for filled boundary equality. +pub fn canonical(shapes: &IntShapes) -> Vec { + let mut paths: Vec = shapes + .iter() + .flatten() + .map(|path| { + let p: Path = path.iter().map(|p| (p.x.to_i64(), p.y.to_i64())).collect(); + normalize(p) + }) + .collect(); + paths.sort(); + paths +} +pub fn normalize(p: Path) -> Path { + if p.is_empty() { + return p; + } + // Lexicographic cyclic minimum handles repeated vertices as well. + let start = (0..p.len()) + .min_by(|&a, &b| { + (0..p.len()) + .map(|i| p[(a + i) % p.len()]) + .cmp((0..p.len()).map(|i| p[(b + i) % p.len()])) + }) + .unwrap(); + (0..p.len()).map(|i| p[(start + i) % p.len()]).collect() +} +pub fn area2(paths: &[Path]) -> i128 { + paths + .iter() + .map(|p| { + p.iter() + .zip(p.iter().cycle().skip(1)) + .take(p.len()) + .map(|(&(x, y), &(u, v))| { + i128::from(x) * i128::from(v) - i128::from(y) * i128::from(u) + }) + .sum::() + }) + .sum() +} + +fn csv(out: &mut impl Write, values: &[String]) -> io::Result<()> { + writeln!( + out, + "{}", + values + .iter() + .map(|v| format!("\"{}\"", v.replace('"', "\"\""))) + .collect::>() + .join(",") + ) +} +pub const HEADER: &str = "schema,fixture_version,runner_version,library_version,library_rev,library_dirty,library_source,runner_source,build_unix,rustc,target,profile,opt_level,debug_info,rustflags,host_os,logical_cpus,run_unix,label,threads_env,mt_compiled,test,case,variant,size,seed,strategy,split_route_hint,fill_route_hint,int,multithreading,precision,rule,mode,input_edges,subj_contours,clip_contours,min_x,min_y,max_x,max_y,input_fingerprint,output_contours,output_edges,output_area2,validated,warmup,samples,iterations,median_ns,min_ns,max_ns,samples_ns"; + +// Hints only: the splitter receives normalized/merged segments, and may change +// strategy on subsequent passes; fill thresholds concern post-split geometry. +fn route(strategy: &str, edges: usize) -> &'static str { + match strategy { + "list" => "list", + "frag" => "frag", + "tree" if edges > 16000 => "frag", + "tree" => "tree", + "auto" if edges < 4000 => "list", + "auto" if edges <= 16000 => "tree", + _ => "frag", + } +} + +pub fn run( + config: &Config, + id: usize, + variant: &str, + size: usize, + f: &Fixture, + deadline: Instant, + out: &mut impl Write, +) -> Result { + let case = super::NAMES[id]; + let (subj, clip) = match convert::(f) { + Ok(input) => input, + Err(e) => { + eprintln!("skip {case} size={size}: {e}"); + return Ok(true); + } + }; + // Reference and all requested strategies are checked outside timing. + let reference = canonical( + &Overlay::from_subj_and_clip_custom( + &subj, + &clip, + Default::default(), + solver("list", false), + ) + .overlay(f.rule, FillRule::NonZero), + ); + for strategy in &config.strategies { + if Instant::now() >= deadline { + return Ok(false); + } + let s = solver(strategy, config.multithreading); + let mut reusable = Overlay::from_subj_and_clip_custom(&subj, &clip, Default::default(), s); + let result = reusable.overlay(f.rule, FillRule::NonZero); + if canonical(&result) != reference { + return Err(format!( + "{case} size={size} i{} {strategy}: geometry differs from List reference", + I::BITS + )); + } + // Check reinit after the splitter has already modified the segment buffer. + reusable.reinit_with_subj_and_clip(&subj, &clip); + if canonical(&reusable.overlay(f.rule, FillRule::NonZero)) != reference { + return Err(format!("{case}: reinit result differs")); + } + if id == 9 { + let mut rotated = f.clone(); + data::rotate(&mut rotated.subj); + data::rotate(&mut rotated.clip); + let (rs, rc) = convert::(&rotated)?; + let actual = canonical( + &Overlay::from_subj_and_clip_custom(&rs, &rc, Default::default(), s) + .overlay(f.rule, FillRule::NonZero), + ); + let mut expected = reference.clone(); + data::rotate(&mut expected); + expected = expected.into_iter().map(normalize).collect(); + expected.sort(); + if actual != expected { + return Err(format!("{case}: exact rotation invariant failed")); + } + } + for mode in &config.modes { + let mut operation = || { + if mode == "cold" { + let mut overlay = Overlay::from_subj_and_clip_custom( + black_box(&subj), + black_box(&clip), + Default::default(), + s, + ); + black_box(overlay.overlay(black_box(f.rule), FillRule::NonZero)); + } else { + reusable.reinit_with_subj_and_clip(black_box(&subj), black_box(&clip)); + black_box(reusable.overlay(black_box(f.rule), FillRule::NonZero)); + } + }; + for _ in 0..config.warmup { + if Instant::now() >= deadline { + return Ok(false); + } + operation(); + } + let mut samples = Vec::with_capacity(config.samples); + for _ in 0..config.samples { + if Instant::now() >= deadline { + return Ok(false); + } + let start = Instant::now(); + for _ in 0..config.iterations { + operation(); + } + samples.push(start.elapsed().as_nanos() / config.iterations as u128); + } + let mut sorted = samples.clone(); + sorted.sort_unstable(); + let median = if sorted.len() % 2 == 0 { + (sorted[sorted.len() / 2 - 1] + sorted[sorted.len() / 2]) / 2 + } else { + sorted[sorted.len() / 2] + }; + let b = f.bounds(); + let fill_hint = match strategy.as_str() { + "list" => "list", + "auto" => "post_split_threshold_8000", + _ => "tree", + }; + let values = vec![ + "2".into(), + "2".into(), + env!("CARGO_PKG_VERSION").into(), + env!("BENCH_LIBRARY_VERSION").into(), + env!("BENCH_LIBRARY_REV").into(), + env!("BENCH_LIBRARY_DIRTY").into(), + env!("BENCH_LIBRARY_SOURCE").into(), + env!("BENCH_RUNNER_SOURCE").into(), + env!("BENCH_BUILD_UNIX").into(), + env!("BENCH_RUSTC").into(), + env!("BENCH_TARGET").into(), + env!("BENCH_PROFILE").into(), + env!("BENCH_OPT_LEVEL").into(), + env!("BENCH_DEBUG").into(), + env!("BENCH_RUSTFLAGS").into(), + std::env::consts::OS.into(), + std::thread::available_parallelism() + .map(|n| n.get().to_string()) + .unwrap_or_else(|_| "unknown".into()), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + .to_string(), + config.label.clone(), + std::env::var("RAYON_NUM_THREADS").unwrap_or_default(), + cfg!(feature = "multithreading").to_string(), + id.to_string(), + case.into(), + variant.into(), + size.to_string(), + config.seed.to_string(), + strategy.clone(), + route(strategy, f.edges()).into(), + fill_hint.into(), + format!("i{}", I::BITS), + config.multithreading.to_string(), + "HIGH".into(), + format!("{:?}", f.rule), + mode.clone(), + f.edges().to_string(), + f.subj.len().to_string(), + f.clip.len().to_string(), + b.0.to_string(), + b.1.to_string(), + b.2.to_string(), + b.3.to_string(), + format!("{:016x}", f.fingerprint()), + reference.len().to_string(), + reference.iter().map(Vec::len).sum::().to_string(), + area2(&reference).to_string(), + "true".into(), + config.warmup.to_string(), + config.samples.to_string(), + config.iterations.to_string(), + median.to_string(), + sorted[0].to_string(), + sorted.last().unwrap().to_string(), + samples + .iter() + .map(u128::to_string) + .collect::>() + .join(";"), + ]; + csv(out, &values).map_err(|e| e.to_string())?; + } + } + Ok(true) +} + +#[cfg(test)] +mod checks { + use super::*; + use i_overlay::core::overlay_rule::OverlayRule; + #[test] + fn integer_coordinate_budget() { + let f = Fixture { + subj: vec![data::rect(16380, 0, 8, 8)], + clip: vec![], + rule: OverlayRule::Union, + }; + assert!(convert::(&f).is_err()); + assert!(convert::(&f).is_ok()); + let f = Fixture { + subj: vec![data::rect(-16384, -16384, 32767, 32767)], + clip: vec![], + rule: OverlayRule::Union, + }; + assert!(convert::(&f).is_ok()); + } +} diff --git a/performance/rust_app/src/test/test_0_checkerboard.rs b/performance/rust_app/src/test/test_0_checkerboard.rs index adeef5b2..ecf6cde4 100644 --- a/performance/rust_app/src/test/test_0_checkerboard.rs +++ b/performance/rust_app/src/test/test_0_checkerboard.rs @@ -1,13 +1,9 @@ -use crate::test::util::{OverlayInt, Util}; -use i_overlay::core::fill_rule::FillRule; -use i_overlay::core::overlay::Overlay; +use super::data::Fixture; +use super::util::Util; use i_overlay::core::overlay_rule::OverlayRule; -use i_overlay::core::solver::Solver; use i_overlay::i_float::int::point::IntPoint; -use std::time::Instant; pub(crate) struct CheckerboardTest; - /* test 0 CheckerboardTest @@ -98,48 +94,10 @@ multithreading off 1024(2095105 6.3) - 4.393880(0.6) */ -// A grid of overlapping squares forming a simple checkerboard pattern. impl CheckerboardTest { - pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { - // 1000 - if Util::skip_if_out_of_range::(n, 15 * n + 10) { - return; - } - - let subj_paths = Util::many_squares( - IntPoint::new(I::ZERO, I::ZERO), - I::from_usize(20), - I::from_usize(30), - n, - ); - let clip_paths = Util::many_squares( - IntPoint::new(I::from_usize(15), I::from_usize(15)), - I::from_usize(20), - I::from_usize(30), - n - 1, - ); - - let it_count = ((scale / (n as f64)) as usize).max(1); - let sq_it_count = it_count * it_count; - - let start = Instant::now(); - - for _i in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); - } - - let duration = start.elapsed(); - let time = duration.as_secs_f64() / sq_it_count as f64; - - let polygons_count = n * n + (n - 1) * (n - 1); - let count_log = (polygons_count as f64).log10(); - let time_log = time.log10(); - - println!( - "{}({} {:.1}) - {:.6}({:.1})", - n, polygons_count, count_log, time, time_log - ); + pub(crate) fn geometry(n: usize) -> Fixture { + let subj = Util::many_squares(IntPoint::new(0i64, 0), 20, 30, n); + let clip = Util::many_squares(IntPoint::new(15i64, 15), 20, 30, n - 1); + Fixture::from_int(subj, clip, OverlayRule::Xor) } } diff --git a/performance/rust_app/src/test/test_10_diagonal.rs b/performance/rust_app/src/test/test_10_diagonal.rs new file mode 100644 index 00000000..d9e7b5b0 --- /dev/null +++ b/performance/rust_app/src/test/test_10_diagonal.rs @@ -0,0 +1,28 @@ +use super::data::Fixture; +use i_overlay::core::overlay_rule::OverlayRule; +pub(crate) struct DiagonalTest; +impl DiagonalTest { + pub(crate) fn geometry(size: usize) -> Fixture { + let n = size as i64; + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + let l = 8 * n + 64; + for i in 0..n { + let y = 16 * i; + let dy = l / 2 + i % 3; + f.subj + .push(vec![(-l, y), (l, y + dy), (l, y + dy + 4), (-l, y + 4)]); + f.clip.push(vec![ + (-l, y + 2), + (l, y + dy + 2), + (l, y + dy + 6), + (-l, y + 6), + ]); + } + f.rule = OverlayRule::Intersect; + f + } +} diff --git a/performance/rust_app/src/test/test_11_collinear.rs b/performance/rust_app/src/test/test_11_collinear.rs new file mode 100644 index 00000000..7e3025f2 --- /dev/null +++ b/performance/rust_app/src/test/test_11_collinear.rs @@ -0,0 +1,21 @@ +use super::data::{rect, Fixture}; +use i_overlay::core::overlay_rule::OverlayRule; +pub(crate) struct CollinearTest; +impl CollinearTest { + pub(crate) fn geometry(size: usize) -> Fixture { + let n = size as i64; + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + for i in 0..n { + let x = 32 * i; + f.subj.push(rect(x, 0, 24, 12)); + f.clip.push(rect(x + 8, 0, 24, 12)); // Partial collinear overlap. + f.clip.push(rect(x, 12, 24, 12)); // Shared boundary. + } + f.rule = OverlayRule::Xor; + f + } +} diff --git a/performance/rust_app/src/test/test_12_cluster.rs b/performance/rust_app/src/test/test_12_cluster.rs new file mode 100644 index 00000000..3939bad9 --- /dev/null +++ b/performance/rust_app/src/test/test_12_cluster.rs @@ -0,0 +1,24 @@ +use super::data::{rect, Fixture, Prng}; +use i_overlay::core::overlay_rule::OverlayRule; +pub(crate) struct ClusterTest; +impl ClusterTest { + pub(crate) fn geometry(size: usize, seed: u64, outlier: bool) -> Fixture { + let n = size as i64; + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + let mut rng = Prng(seed); + for _ in 0..n { + let x = (rng.next() % 512) as i64 - 256; + let y = (rng.next() % 512) as i64 - 256; + f.subj.push(rect(x, y, 64, 64)); + f.clip.push(rect(x + 16, y + 16, 64, 64)); + } + // Both paired cases have exactly the same edge count. + let x = if outlier { 12000 } else { 0 }; + f.subj.push(rect(x, x, 8, 8)); + f + } +} diff --git a/performance/rust_app/src/test/test_13_seeded.rs b/performance/rust_app/src/test/test_13_seeded.rs new file mode 100644 index 00000000..38506c4d --- /dev/null +++ b/performance/rust_app/src/test/test_13_seeded.rs @@ -0,0 +1,28 @@ +use super::data::{rect, Fixture, Prng}; +use i_overlay::core::overlay_rule::OverlayRule; +pub(crate) struct SeededTest; +impl SeededTest { + pub(crate) fn geometry(size: usize, seed: u64) -> Fixture { + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + let mut rng = Prng(seed); + // Exactly four draws per rectangle, alternating subject and clip. + // Modulo mapping is intentional (no distribution/version dependency). + for i in 0..2 * size { + let x = (rng.next() % 4096) as i64 - 2048; + let y = (rng.next() % 4096) as i64 - 2048; + let w = 8 + (rng.next() % 256) as i64; + let h = 8 + (rng.next() % 256) as i64; + if i % 2 == 0 { + f.subj.push(rect(x, y, w, h)); + } else { + f.clip.push(rect(x, y, w, h)); + } + } + f.rule = OverlayRule::Xor; + f + } +} diff --git a/performance/rust_app/src/test/test_1_not_overlap.rs b/performance/rust_app/src/test/test_1_not_overlap.rs index d447e08f..57e99e92 100644 --- a/performance/rust_app/src/test/test_1_not_overlap.rs +++ b/performance/rust_app/src/test/test_1_not_overlap.rs @@ -1,10 +1,7 @@ -use crate::test::util::{OverlayInt, Util}; -use i_overlay::core::fill_rule::FillRule; -use i_overlay::core::overlay::Overlay; +use super::data::Fixture; +use super::util::Util; use i_overlay::core::overlay_rule::OverlayRule; -use i_overlay::core::solver::Solver; use i_overlay::i_float::int::point::IntPoint; -use std::time::Instant; pub(crate) struct NotOverlapTest; /* @@ -102,43 +99,22 @@ multithreading off */ -// A grid of not overlapping squares. impl NotOverlapTest { - pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { - // 1000 - if Util::skip_if_out_of_range::(n, 30 * n + 10) { - return; + pub(crate) fn geometry(n: usize, line: bool) -> Fixture { + if line { + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + for i in 0..n as i64 { + f.subj.push(super::data::rect(32 * i, 0, 8, 8)); + f.clip.push(super::data::rect(32 * i + 16, 12, 8, 8)); + } + return f; } - - let subj_paths = Util::many_squares( - IntPoint::new(I::ZERO, I::ZERO), - I::from_usize(10), - I::from_usize(30), - n, - ); - let clip_paths = Util::many_squares( - IntPoint::new(I::from_usize(15), I::from_usize(15)), - I::from_usize(10), - I::from_usize(30), - n - 1, - ); - - let it_count = ((scale / (n as f64)) as usize).max(1); - let sq_it_count = it_count * it_count; - - let start = Instant::now(); - - for _i in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); - } - - let duration = start.elapsed(); - let time = duration.as_secs_f64() / sq_it_count as f64; - - let polygons_count = n * n + (n - 1) * (n - 1); - - println!("{:.1} - {:.6}", polygons_count, time); + let subj = Util::many_squares(IntPoint::new(0i64, 0), 10, 30, n); + let clip = Util::many_squares(IntPoint::new(15i64, 15), 10, 30, n - 1); + Fixture::from_int(subj, clip, OverlayRule::Union) } } diff --git a/performance/rust_app/src/test/test_2_lines_net.rs b/performance/rust_app/src/test/test_2_lines_net.rs index 3d98e92d..6e501f5a 100644 --- a/performance/rust_app/src/test/test_2_lines_net.rs +++ b/performance/rust_app/src/test/test_2_lines_net.rs @@ -1,12 +1,8 @@ -use crate::test::util::{OverlayInt, Util}; -use i_overlay::core::fill_rule::FillRule; -use i_overlay::core::overlay::Overlay; +use super::data::Fixture; +use super::util::Util; use i_overlay::core::overlay_rule::OverlayRule; -use i_overlay::core::solver::Solver; -use std::time::Instant; pub(crate) struct LinesNetTest; - /* test 2 LinesNetTest @@ -102,33 +98,12 @@ multithreading off */ -// A grid is formed by the intersection of a set of vertical and horizontal lines. impl LinesNetTest { - pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { - // 500 - if Util::skip_if_out_of_range::(n, 10 * n + 10) { - return; - } - - let subj_paths = Util::many_lines_x(I::from_usize(20), n); - let clip_paths = Util::many_lines_y(I::from_usize(20), n); - - let it_count = ((scale / (n as f64)) as usize).max(1); - let sq_it_count = it_count * it_count; - - let start = Instant::now(); - - for _ in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); - } - - let duration = start.elapsed(); - let time = duration.as_secs_f64() / sq_it_count as f64; - - let polygons_count = 2 * n; - - println!("{} - {:.6}", polygons_count, time); + pub(crate) fn geometry(n: usize) -> Fixture { + Fixture::from_int( + Util::many_lines_x(20i64, n), + Util::many_lines_y(20i64, n), + OverlayRule::Intersect, + ) } } diff --git a/performance/rust_app/src/test/test_3_spiral.rs b/performance/rust_app/src/test/test_3_spiral.rs index 7c2e8b8f..e5791858 100644 --- a/performance/rust_app/src/test/test_3_spiral.rs +++ b/performance/rust_app/src/test/test_3_spiral.rs @@ -1,13 +1,9 @@ -use crate::test::util::{OverlayInt, Util}; -use i_overlay::core::fill_rule::FillRule; +use super::data::Fixture; +use super::runner::Engine; +use super::util::Util; use i_overlay::core::overlay_rule::OverlayRule; -use i_overlay::core::solver::Solver; -use i_overlay::float::overlay::FloatOverlay; -use i_overlay::i_float::float::point::FloatPoint; -use std::time::Instant; pub(crate) struct SpiralTest; - /* test 3 SpiralTest @@ -153,31 +149,54 @@ multithreading off */ -// Two irregular self-intersecting polygons are generated, the vertices of which are defined by a fixed radius and angle. impl SpiralTest { - pub(crate) fn run(n: usize, solver: Solver, scale: f64) { - // 1000 - let subj_path = Util::spiral(n, 100.0); - - let it_count = ((scale / (n as f64)) as usize).max(1); - let sq_it_count = it_count * it_count; - - let start = Instant::now(); - - for _ in 0..sq_it_count { - let _ = FloatOverlay::, I>::from_subj_custom( - &subj_path, - Default::default(), - solver, - ) - .overlay(OverlayRule::Subject, FillRule::NonZero); + pub(crate) fn geometry(n: usize, integer: bool) -> Fixture { + if integer { + return Self::integer_geometry(n); } - - let duration = start.elapsed(); - let time = duration.as_secs_f64() / sq_it_count as f64; - - let polygons_count = n; - - println!("{} - {:.6}", polygons_count, time); + let path = Util::spiral(n, 100.0) + .into_iter() + .map(|p| [p.x, p.y]) + .collect(); + Fixture::from_float::(vec![path], vec![], OverlayRule::Subject) + } + fn integer_geometry(size: usize) -> Fixture { + let n = size as i64; + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Difference, + }; + // Square spiral ribbon: offset each orthogonal turn by 2 units. + let directions = [(1, 0), (0, 1), (-1, 0), (0, -1)]; + let mut center = vec![(0, 0)]; + for i in 0..size { + let (dx, dy) = directions[i % 4]; + let step = 16 * (i as i64 / 2 + 1); + let (x, y) = *center.last().unwrap(); + center.push((x + dx * step, y + dy * step)); + } + let mut left = Vec::new(); + let mut right = Vec::new(); + for (i, &(x, y)) in center.iter().enumerate() { + let before = directions[i.saturating_sub(1) % 4]; + let after = directions[i.min(size - 1) % 4]; + let normal = if i == 0 { + (-after.1, after.0) + } else if i == size { + (-before.1, before.0) + } else { + (-before.1 - after.1, before.0 + after.0) + }; + left.push((x + 2 * normal.0, y + 2 * normal.1)); + right.push((x - 2 * normal.0, y - 2 * normal.1)); + } + right.reverse(); + left.extend(right); + f.subj.push(left); + f.clip + .push(super::data::rect(-4, -16 * n - 4, 8, 32 * n + 8)); + f.rule = OverlayRule::Difference; + f } } diff --git a/performance/rust_app/src/test/test_4_windows.rs b/performance/rust_app/src/test/test_4_windows.rs index 5c460c40..9235d6b6 100644 --- a/performance/rust_app/src/test/test_4_windows.rs +++ b/performance/rust_app/src/test/test_4_windows.rs @@ -1,10 +1,7 @@ -use crate::test::util::{OverlayInt, Util}; -use i_overlay::core::fill_rule::FillRule; -use i_overlay::core::overlay::Overlay; +use super::data::Fixture; +use super::util::Util; use i_overlay::core::overlay_rule::OverlayRule; -use i_overlay::core::solver::Solver; use i_overlay::i_float::int::point::IntPoint; -use std::time::Instant; pub(crate) struct WindowsTest; /* @@ -92,36 +89,10 @@ i64 */ -// A grid of square frames, each with a smaller square cutout in the center. impl WindowsTest { - pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { - // 500 - if Util::skip_if_out_of_range::(n, 15 * n + 20) { - return; - } - - let offset = I::from_usize(30); - let x = I::from_usize(n) * offset / I::TWO; - let origin = IntPoint::new(-x, -x); - let (subj_paths, clip_paths) = - Util::many_windows(origin, I::from_usize(20), I::from_usize(10), offset, n); - - let it_count = ((scale / (n as f64)) as usize).max(1); - let sq_it_count = it_count * it_count; - - let start = Instant::now(); - - for _ in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); - } - - let duration = start.elapsed(); - let time = duration.as_secs_f64() / sq_it_count as f64; - - let polygons_count = 2 * n * n; - - println!("{} - {:.6}", polygons_count, time); + pub(crate) fn geometry(n: usize) -> Fixture { + let x = n as i64 * 30 / 2; + let (subj, clip) = Util::many_windows(IntPoint::new(-x, -x), 20, 10, 30, n); + Fixture::from_int(subj, clip, OverlayRule::Difference) } } diff --git a/performance/rust_app/src/test/test_5_nested_squares.rs b/performance/rust_app/src/test/test_5_nested_squares.rs index 994aac98..e436b0b1 100644 --- a/performance/rust_app/src/test/test_5_nested_squares.rs +++ b/performance/rust_app/src/test/test_5_nested_squares.rs @@ -1,12 +1,8 @@ -use crate::test::util::{OverlayInt, Util}; -use i_overlay::core::fill_rule::FillRule; -use i_overlay::core::overlay::Overlay; +use super::data::Fixture; +use super::util::Util; use i_overlay::core::overlay_rule::OverlayRule; -use i_overlay::core::solver::Solver; -use std::time::Instant; pub(crate) struct CrossTest; - /* test 5 CrossTest @@ -119,31 +115,9 @@ i64 */ -// A series of concentric squares, each progressively larger than the last. impl CrossTest { - pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { - // 500 - if Util::skip_if_out_of_range::(n, 8 * n + 8) { - return; - } - - let (subj_paths, clip_paths) = Util::concentric_squares(I::from_usize(2), n); - - let it_count = ((scale / (n as f64)) as usize).max(1); - let sq_it_count = it_count * it_count; - - let start = Instant::now(); - - for _ in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); - } - let duration = start.elapsed(); - let time = duration.as_secs_f64() / sq_it_count as f64; - - let polygons_count = 2 * n; - - println!("{} - {:.6}", polygons_count, time); + pub(crate) fn geometry(n: usize) -> Fixture { + let (subj, clip) = Util::concentric_squares(2i64, n); + Fixture::from_int(subj, clip, OverlayRule::Xor) } } diff --git a/performance/rust_app/src/test/test_6_corrosion.rs b/performance/rust_app/src/test/test_6_corrosion.rs index b7d7c951..2f714558 100644 --- a/performance/rust_app/src/test/test_6_corrosion.rs +++ b/performance/rust_app/src/test/test_6_corrosion.rs @@ -1,13 +1,10 @@ -use crate::test::util::OverlayInt; -use i_overlay::core::fill_rule::FillRule; +use super::data::Fixture; +use super::runner::Engine; use i_overlay::core::overlay_rule::OverlayRule; -use i_overlay::core::solver::Solver; -use i_overlay::float::overlay::FloatOverlay; use std::f64::consts::PI; -use std::time::Instant; +type FloatGeometry = (Vec>, Vec>); pub(crate) struct CorrosionTest; - /* test 6 CorrosionTest @@ -92,33 +89,12 @@ i64 512 - 5.351266 */ -// A series of concentric squares, each progressively larger than the last. impl CorrosionTest { - pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { - // 500 - let (subj_paths, clip_paths) = Self::geometry(100.0, n); - - let it_count = ((scale / (n as f64)) as usize).max(1); - let sq_it_count = it_count * it_count; - - let start = Instant::now(); - - for _ in 0..sq_it_count { - let mut overlay = FloatOverlay::<[f64; 2], I>::from_subj_and_clip_custom( - &subj_paths, - &clip_paths, - Default::default(), - solver, - ); - let _res = overlay.overlay(rule, FillRule::NonZero); - } - let duration = start.elapsed(); - let time = duration.as_secs_f64() / sq_it_count as f64; - - println!("{} - {:.6}", n, time); + pub(crate) fn geometry(n: usize) -> Fixture { + let (subj, clip) = Self::float_geometry(100.0, n); + Fixture::from_float::(subj, clip, OverlayRule::Difference) } - - fn geometry(size: f64, count: usize) -> (Vec>, Vec>) { + fn float_geometry(size: f64, count: usize) -> FloatGeometry { let subj_radius = 0.4 * size; let subj_step = size; @@ -137,13 +113,11 @@ impl CorrosionTest { let mut paths = Vec::with_capacity(count); for i in 0..count { - let mut index = i; let mut x = -offset; - for _ in 0..count { + for index in i..i + count { let count = (index % 5) + 3; paths.push(Self::shape([x, y], radius, count)); x += step; - index += 1; } y += step; } diff --git a/performance/rust_app/src/test/test_7_concentric.rs b/performance/rust_app/src/test/test_7_concentric.rs index 680c7d6f..bd920c87 100644 --- a/performance/rust_app/src/test/test_7_concentric.rs +++ b/performance/rust_app/src/test/test_7_concentric.rs @@ -1,13 +1,10 @@ -use crate::test::util::OverlayInt; -use i_overlay::core::fill_rule::FillRule; +use super::data::Fixture; +use super::runner::Engine; use i_overlay::core::overlay_rule::OverlayRule; -use i_overlay::core::solver::Solver; -use i_overlay::float::overlay::FloatOverlay; use std::f64::consts::PI; -use std::time::Instant; +type FloatGeometry = (Vec>, Vec>); pub(crate) struct ConcentricTest; - /* test 7 ConcentricTest @@ -94,32 +91,12 @@ i64 */ -// A series of concentric squares, each progressively larger than the last. impl ConcentricTest { - pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { - let (subj_paths, clip_paths) = Self::geometry(100.0, n); - - let it_count = ((scale / (n as f64)) as usize).max(1); - let sq_it_count = it_count * it_count; - - let start = Instant::now(); - - for _ in 0..sq_it_count { - let mut overlay = FloatOverlay::<[f64; 2], I>::from_subj_and_clip_custom( - &subj_paths, - &clip_paths, - Default::default(), - solver, - ); - let _res = overlay.overlay(rule, FillRule::NonZero); - } - let duration = start.elapsed(); - let time = duration.as_secs_f64() / sq_it_count as f64; - - println!("{} - {:.6}", n, time); + pub(crate) fn geometry(n: usize) -> Fixture { + let (subj, clip) = Self::float_geometry(100.0, n); + Fixture::from_float::(subj, clip, OverlayRule::Intersect) } - - fn geometry(size: f64, count: usize) -> (Vec>, Vec>) { + fn float_geometry(size: f64, count: usize) -> FloatGeometry { let mut clip = Vec::with_capacity(count); let mut subj = Vec::with_capacity(count); diff --git a/performance/rust_app/src/test/test_8_wind_mill.rs b/performance/rust_app/src/test/test_8_wind_mill.rs index c3182724..634cb1e8 100644 --- a/performance/rust_app/src/test/test_8_wind_mill.rs +++ b/performance/rust_app/src/test/test_8_wind_mill.rs @@ -1,15 +1,10 @@ -use crate::test::util::{OverlayInt, Util}; -use i_overlay::core::fill_rule::FillRule; -use i_overlay::core::overlay::Overlay; +use super::data::Fixture; use i_overlay::core::overlay_rule::OverlayRule; -use i_overlay::core::solver::Solver; use i_overlay::i_float::int::number::int::IntNumber; use i_overlay::i_float::int::point::IntPoint; use i_overlay::i_shape::int::shape::IntContour; -use std::time::Instant; pub(crate) struct WindMillTest; - /* test 8 WindMillTest @@ -94,42 +89,14 @@ i64 */ impl WindMillTest { - pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { - if Util::skip_if_out_of_range::(n, 80 * n + 80) { - return; - } - - let (subj_paths, clip_paths) = Self::geometry(I::from_usize(80), n); - - let it_count = ((scale / (n as f64)) as usize).max(1); - let sq_it_count = it_count * it_count; - - let start = Instant::now(); - - for _ in 0..sq_it_count { - let _ = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); - } - - let duration = start.elapsed(); - let time = duration.as_secs_f64() / sq_it_count as f64; - - println!("{} - {:.6}", n, time); + pub(crate) fn geometry(n: usize) -> Fixture { + let (subj, clip) = Self::int_geometry(80i64, n); + Fixture::from_int(subj, clip, OverlayRule::Intersect) } - - pub(crate) fn validate(n: usize, rule: OverlayRule, solver: Solver) { - let (subj_paths, clip_paths) = Self::geometry(I::from_usize(80), n); - - let res = - Overlay::with_contours_custom(&subj_paths, &clip_paths, Default::default(), solver) - .overlay(rule, FillRule::NonZero); - - assert_eq!(res.len(), n * n); - println!("result validation PASS"); - } - - fn geometry(size: I, count: usize) -> (Vec>, Vec>) { + fn int_geometry( + size: I, + count: usize, + ) -> (Vec>, Vec>) { let mut subj_paths = Vec::with_capacity(4 * count * count); let mut clip_paths = Vec::with_capacity(4 * count * count); diff --git a/performance/rust_app/src/test/test_9_orientation.rs b/performance/rust_app/src/test/test_9_orientation.rs new file mode 100644 index 00000000..9b0e5a2a --- /dev/null +++ b/performance/rust_app/src/test/test_9_orientation.rs @@ -0,0 +1,23 @@ +use super::data::{rect, rotate, Fixture}; +use i_overlay::core::overlay_rule::OverlayRule; +pub(crate) struct OrientationTest; +impl OrientationTest { + pub(crate) fn geometry(size: usize, vertical: bool) -> Fixture { + let n = size as i64; + let mut f = Fixture { + subj: vec![], + clip: vec![], + rule: OverlayRule::Union, + }; + for i in 0..n { + f.subj.push(rect(-8 * n, 16 * i, 16 * n, 6)); + f.clip.push(rect(-8 * n + 4, 16 * i + 3, 16 * n, 6)); + } + f.rule = OverlayRule::Xor; + if vertical { + rotate(&mut f.subj); + rotate(&mut f.clip); + } + f + } +} diff --git a/performance/rust_app/src/test/util.rs b/performance/rust_app/src/test/util.rs index 08e6da44..d2acf23c 100644 --- a/performance/rust_app/src/test/util.rs +++ b/performance/rust_app/src/test/util.rs @@ -1,35 +1,13 @@ -use i_key_sort::sort::key::SortKey; use i_overlay::i_float::float::point::FloatPoint; use i_overlay::i_float::int::number::int::IntNumber; use i_overlay::i_float::int::point::IntPoint; use i_overlay::i_shape::base::data::Contour; use i_overlay::i_shape::int::path::IntPath; -use i_tree::{Expiration, LayoutNumber}; use std::f64::consts::PI; pub(super) struct Util; -pub(crate) trait OverlayInt: IntNumber + Expiration + LayoutNumber + SortKey {} - -impl OverlayInt for I where I: IntNumber + Expiration + LayoutNumber + SortKey {} - impl Util { - pub(super) fn skip_if_out_of_range(n: usize, max_abs_coord: usize) -> bool { - let max = I::MAX.to_usize(); - if max_abs_coord <= max { - return false; - } - - println!( - "{} - skipped (requires coordinate {}, i{} max {})", - n, - max_abs_coord, - I::BITS, - max - ); - true - } - pub(super) fn many_squares( start: IntPoint, size: I, From c87b42b9faca6c912044593d251063e8fa80ee5c Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Mon, 12 Oct 2026 11:51:44 +0300 Subject: [PATCH 2/7] performance base --- performance/rust_app/README.md | 79 +++++++++- performance/rust_app/quick.py | 188 ++++++++++++++++++++++++ performance/rust_app/src/config.rs | 18 ++- performance/rust_app/src/main.rs | 4 +- performance/rust_app/src/test/runner.rs | 8 +- 5 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 performance/rust_app/quick.py diff --git a/performance/rust_app/README.md b/performance/rust_app/README.md index 70c168e8..8ce598b0 100644 --- a/performance/rust_app/README.md +++ b/performance/rust_app/README.md @@ -86,7 +86,7 @@ complex=true, как прежде, выбирает серию размеров Перед замером каждый результат сравнивается с List-эталоном и проверяется reinit. Сравниваются ориентированные выходные границы с независимостью от порядка фигур/контуров и циклической начальной вершины. Направления контуров сохраняются, отверстия учитываются. Это строгий компаратор границ для данного корпуса; расхождения не скрываются допуском. Для test_9 проверяется точное совпадение геометрии после поворота на 90°; превосходство horizontal не предполагается. Все пересечения этой пары целочисленные. -Мягкий `--budget-seconds` по умолчанию 30; сборка не входит в бюджет. Проверки выполняются между комбинациями, warm-up операциями и выборками; одна операция/проверка корректности не прерывается. При исчерпании бюджета незавершённая строка и остальные комбинации пропускаются, stderr сообщает причину, завершённые строки сохраняются (exit 0). Проверяйте полноту CSV. +Мягкий `--budget-seconds` по умолчанию 30; значение **0 полностью отключает бюджет**; сборка не входит в бюджет. Проверки выполняются между комбинациями, warm-up операциями и выборками; одна операция/проверка корректности не прерывается. При исчерпании бюджета незавершённая строка и остальные комбинации пропускаются, stderr сообщает причину, завершённые строки сохраняются (exit 0). Проверяйте полноту CSV. ## Воспроизводимость и результаты @@ -120,3 +120,80 @@ Golden test_13 при count=8: seed=0 → 77ac91e43630115d, seed=42 → 95eba339 В текущей библиотеке Auto split: List при <4000 сегментов, Tree при 4000..=16000, Frag при >16000. Запрошенный Tree также переходит в Frag при >16000. Fill Auto имеет отдельный порог <8000 после splitting; Tree/Frag используют tree-fill. В CSV strategy — запрос, split_route_hint — оценка по исходным рёбрам; нормализация, слияние и повторные проходы могут изменить сегменты. Fill route также не инструментирован. Tree/Frag на больших данных нельзя выдавать за сравнение разных split-алгоритмов. Библиотека не изменена. Малые smoke-данные обычно не достигают parallel-sort порога 32768; multithreading=true не доказывает фактическое распараллеливание. Test_10 содержит почти параллельные полосы с частичным перекрытием и без плотной сетки пересечений; плотные пересечения уже покрывает test_2. Строгие тесты используют размеры 1/4/8; большие или иные float-данные могут выявить новые расхождения — тогда runner сообщит ошибку. + + +## Короткий профиль для сравнения изменений + +```bash +# Выполнять из performance/rust_app. Требуется Python 3. +python3 quick.py --output results/base.csv +# После изменения библиотеки — тот же фиксированный набор: +python3 quick.py --output results/experiment.csv --compare results/base.csv +``` + +`quick-v1` последовательно запускает номера 0..13 с заранее подобранными размерами. +Два движка i32/i64, четыре стратегии, cold/reuse, seed=42; 2 warm-up и 7 выборок +по одной операции. Итого **304 строки**. Потоки выключены. Сборка release выполняется +один раз перед набором; зависимости берутся offline из Cargo.lock. + +| Тест | count | Варианты | +|---:|---|---| +| 0 | 24 | checkerboard | +| 1 | 499,500,2001 | line: 3992/4000/16008 исходных рёбер | +| 2 | 48 | lines_net | +| 3 | 256 | float и integer | +| 4 | 32 | windows | +| 5 | 192 | nested_squares | +| 6 | 10 | corrosion | +| 7 | 8 | concentric | +| 8 | 18 | wind_mill | +| 9 | 256 | horizontal и vertical | +| 10 | 160 | diagonal | +| 11 | 1024 | collinear | +| 12 | 128 | cluster и outlier | +| 13 | 512 | seeded | + +Цель подбора: базовая версия на машине измерения выполняет **весь номер теста** +(оба движка, все стратегии, режимы, размеры и варианты, вместе с генерацией, +проверками и прогревом) быстрее 0,5 с. Это характеристика измеренного baseline, +**не тайм-аут и не гарантия на любой машине**. Размеры никогда не подстраиваются +автоматически. Изменение, которое замедлит тест, будет полностью измерено; +строки не отбрасываются. Профиль передаёт `--budget-seconds 0`. +Сборка и первый запуск свежего бинарника (включая возможную проверку ОС) +учитываются отдельно; перед набором выполняется `--help`. + +Рядом с CSV сохраняется JSON: команды, фиксированный профиль, SHA-256 quick.py, +модель CPU, объём памяти, revision, время сборки/первого запуска, полное время +каждого номера, полнота результата и stderr. Существующие файлы не перезаписываются. +CSV сохраняет все исходные выборки и metadata Rust-стенда. При ошибке остаётся +JSON с complete=false; такой запуск не является baseline. + +`--compare` требует одинаковые ключи, input fingerprint, выходные сводки, +runner_source, компилятор/target, параметры сборки и протокол измерений. +Меняться может исходник библиотеки. Различия в структуре контура или выходной +сводке требуют отдельной проверки; молча сравнивать их нельзя. Итоговый +`.comparison.csv` содержит baseline_ns/current_ns, change_percent и speedup. +Положительный change_percent означает замедление. Сравнивайте на одной машине +с одинаковой нагрузкой и энергорежимом; небольшие различия проверяйте повторным +запуском. Профиль не покрывает i16, многопоточность и весь диапазон больших данных. + + +### Сохранённый baseline quick-v1 + +`results/baselines/2026-10-12-e40116b6/baseline.csv` — Apple M4, 24 GiB, +revision `e40116b6d70157e7393fd2b64b28c01d82049a74`, library_source +`e4461ef685d79b48`, runner_source `579e46a30c4f8a5c`, библиотека без изменений. +304 строки; весь набор 2,879 с, контрольный повтор 2,876 с; максимальный +номер теста в обоих прогонах 0,306 с. Рядом: repeat.csv, comparison, +case-times.csv, summary.json, метаданные запусков и снимок изменений стенда. +Эти результаты локальные: каталог results игнорируется Git. + +```bash +python3 quick.py --output results/experiment.csv \ + --compare results/baselines/2026-10-12-e40116b6/baseline.csv +``` + +В контрольном повторе медиана абсолютного изменения времени составила 1,82%, +p95 — 8,34%, максимум отдельной строки — 34,44%. Это быстрый регрессионный +профиль: единичное небольшое ускорение/замедление не считается доказанным. +Для спорных результатов повторяйте ту же команду с новым именем CSV. diff --git a/performance/rust_app/quick.py b/performance/rust_app/quick.py new file mode 100644 index 00000000..87ffc6ea --- /dev/null +++ b/performance/rust_app/quick.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Fixed quick-v1 corpus. Sizes never adapt to the machine or measured speed.""" +import argparse +import csv +import hashlib +import io +import json +import platform +import statistics +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +PROFILE = [ + (0, "24", []), + (1, "499,500,2001", ["--layout", "line"]), + (2, "48", []), + (3, "256", ["--spiral", "all"]), + (4, "32", []), + (5, "192", []), + (6, "10", []), + (7, "8", []), + (8, "18", []), + (9, "256", ["--orientation", "all"]), + (10, "160", []), + (11, "1024", []), + (12, "128", ["--outlier", "all"]), + (13, "512", []), +] +COMMON = ["--int", "i32,i64", "--strategy", "all", "--mode", "all", + "--seed", "42", "--warmup", "2", "--samples", "7", "--iterations", "1", + "--multithreading", "false", "--budget-seconds", "0"] +KEY = ["test", "variant", "size", "int", "strategy", "mode", "seed"] +CONTRACT = ["schema", "fixture_version", "runner_source", "rustc", "target", + "profile", "opt_level", "debug_info", "rustflags", "mt_compiled", + "multithreading", "precision", "rule", "input_edges", "input_fingerprint", + "output_contours", "output_edges", "output_area2", "warmup", "samples", + "iterations"] + + +def read_rows(text): + reader = csv.DictReader(io.StringIO(text)) + rows = list(reader) + if not rows or not reader.fieldnames: + raise ValueError("empty CSV") + seen = set() + for row in rows: + if None in row or None in row.values() or row["validated"] != "true": + raise ValueError("malformed or unvalidated CSV row") + key = tuple(row[k] for k in KEY) + if key in seen: + raise ValueError(f"duplicate row: {key}") + seen.add(key) + samples = list(map(int, row["samples_ns"].split(";"))) + if (len(samples) != int(row["samples"]) + or int(statistics.median(samples)) != int(row["median_ns"]) + or min(samples) != int(row["min_ns"]) + or max(samples) != int(row["max_ns"])): + raise ValueError(f"invalid sample statistics: {key}") + return reader.fieldnames, rows + + +def compare(before, after): + old = {tuple(r[k] for k in KEY): r for r in before} + new = {tuple(r[k] for k in KEY): r for r in after} + if old.keys() != new.keys(): + raise ValueError("comparison requires exactly the same cases, sizes, engines and modes") + result = [] + for key, row in new.items(): + ref = old[key] + changed = [k for k in CONTRACT if row[k] != ref[k]] + if changed: + raise ValueError(f"incompatible comparison {key}: {', '.join(changed)}") + a, b = int(ref["median_ns"]), int(row["median_ns"]) + if a <= 0 or b <= 0: + raise ValueError(f"nonpositive median: {key}") + result.append({**{k: row[k] for k in KEY}, "baseline_ns": a, "current_ns": b, + "change_percent": f"{100 * (b / a - 1):.3f}", + "speedup": f"{a / b:.4f}"}) + return result + + +def capture(cmd): + result = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True) + return result.stdout.strip() if result.returncode == 0 else "unavailable" + + +def write_csv(path, fields, rows): + with path.open("x", newline="") as file: + writer = csv.DictWriter(file, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", required=True, type=Path, help="new CSV; existing files are never overwritten") + parser.add_argument("--compare", type=Path, help="baseline CSV with the same measurement contract") + args = parser.parse_args() + output = args.output.resolve() + meta_path = output.with_suffix(".json") + diff_path = output.with_suffix(".comparison.csv") + for path in [output, meta_path, diff_path]: + if path.exists(): + raise ValueError(f"refusing to overwrite {path}") + before = read_rows(args.compare.read_text())[1] if args.compare else None + output.parent.mkdir(parents=True, exist_ok=True) + build = ["cargo", "build", "--release", "--offline", "--locked", "--bin", "rust_app", + "--no-default-features", "--message-format=json-render-diagnostics"] + start = time.perf_counter() + built = subprocess.run(build, cwd=ROOT, stdout=subprocess.PIPE, text=True) + if built.returncode: + print(built.stdout, file=sys.stderr) + raise RuntimeError("release build failed") + build_seconds = time.perf_counter() - start + artifacts = [json.loads(line) for line in built.stdout.splitlines() if line.startswith("{")] + binary = next(a["executable"] for a in reversed(artifacts) + if a.get("reason") == "compiler-artifact" and a.get("executable") + and a["target"]["name"] == "rust_app") + # First launch of a freshly linked executable can include OS verification. + # Record it separately, before the 14 numbered tests and their warm-ups. + start = time.perf_counter() + subprocess.run([binary, "--help"], check=True, capture_output=True) + launch_seconds = time.perf_counter() - start + host = {"platform": platform.platform(), "machine": platform.machine(), + "cpu": capture(["/usr/sbin/sysctl", "-n", "machdep.cpu.brand_string"]), + "memory_bytes": capture(["/usr/sbin/sysctl", "-n", "hw.memsize"]), + "logical_cpus": capture(["/usr/sbin/sysctl", "-n", "hw.ncpu"])} + label = f"quick-v1 {host['cpu']}" + metadata = {"profile": "quick-v1", "profile_cases": PROFILE, "common_args": COMMON, + "profile_script_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + "git_revision": capture(["git", "rev-parse", "HEAD"]), "host": host, + "started_unix": time.time(), "build_seconds": build_seconds, + "first_launch_seconds": launch_seconds, "cases": [], "complete": False} + rows, fields = [], None + suite_start = time.perf_counter() + try: + for test, counts, options in PROFILE: + command = [binary, "--test", str(test), "--count", counts, *COMMON, + *options, "--label", label] + start = time.perf_counter() + # Deliberately no timeout, adaptive sizing, or per-case time budget. + run = subprocess.run(command, cwd=ROOT, capture_output=True, text=True) + seconds = time.perf_counter() - start + record = {"test": test, "command": command, "wall_seconds": seconds, + "exit_code": run.returncode, "stderr": run.stderr} + metadata["cases"].append(record) + if run.returncode or run.stderr: + raise RuntimeError(f"test {test} failed or skipped data: {run.stderr}") + header, case_rows = read_rows(run.stdout) + variants = {1: ["line"], 3: ["float", "integer"], + 9: ["horizontal", "vertical"], 12: ["cluster", "outlier"]}.get(test, ["default"]) + expected = {(str(test), v, n, i, s, m, "42") + for v in variants for n in counts.split(",") for i in ["i32", "i64"] + for s in ["auto", "list", "tree", "frag"] for m in ["cold", "reuse"]} + if {tuple(r[k] for k in KEY) for r in case_rows} != expected: + raise ValueError(f"test {test}: incomplete or unexpected matrix") + if fields is not None and fields != header: + raise ValueError("CSV header changed between tests") + fields = header + rows.extend(case_rows) + record["rows"] = len(case_rows) + record["input_edges"] = sorted({int(r["input_edges"]) for r in case_rows}) + print(f"test {test:2}: {seconds:.3f}s, {len(case_rows)} rows" + + (" (above baseline target; retained)" if seconds > 0.5 else ""), file=sys.stderr) + metadata["complete"] = True + finally: + metadata["suite_seconds"] = time.perf_counter() - suite_start + metadata["rows"] = len(rows) + with meta_path.open("x") as file: + json.dump(metadata, file, indent=2) + file.write("\n") + write_csv(output, fields, rows) + if before is not None: + changes = compare(before, rows) + write_csv(diff_path, list(changes[0]), changes) + print(f"Comparison: {diff_path} (positive change_percent = slower)", file=sys.stderr) + print(f"Saved {len(rows)} rows: {output}\nSuite: {metadata['suite_seconds']:.3f}s", file=sys.stderr) + + +if __name__ == "__main__": + try: + main() + except (ValueError, RuntimeError, OSError, subprocess.CalledProcessError) as error: + print(f"error: {error}", file=sys.stderr) + sys.exit(1) diff --git a/performance/rust_app/src/config.rs b/performance/rust_app/src/config.rs index 1da5d5f5..13acb2af 100644 --- a/performance/rust_app/src/config.rs +++ b/performance/rust_app/src/config.rs @@ -13,7 +13,7 @@ pub struct Config { pub warmup: usize, pub samples: usize, pub iterations: usize, - pub budget: Duration, + pub budget: Option, pub complex: bool, pub orientation: String, pub outlier: String, @@ -41,7 +41,7 @@ const HELP: &str = "iOverlay numbered performance tests (release)\n\ --complex false|true (true uses 2,4,8,16,32,64, caps quadratic tests)\n\ --strategy auto|list|tree|frag|all (default all) --int i16|i32|i64|all (default i32)\n\ --mode cold|reuse|all --multithreading false|true (requires feature multithreading)\n\ ---seed 0 --warmup 2 --samples 7 --iterations 3 --budget-seconds 30 --label local\n\ +--seed 0 --warmup 2 --samples 7 --iterations 3 --budget-seconds 30 (0 disables budget) --label local\n\ --orientation horizontal|vertical|all (test 9; default all)\n\ --outlier false|true|all (test 12; default all)\n\ --spiral float|integer|all (test 3; default float)\n\ @@ -222,7 +222,10 @@ pub fn parse(args: impl Iterator) -> Result, Strin warmup: number("warmup", "2", 1, 100)?, samples: number("samples", "7", 3, 101)?, iterations: number("iterations", "3", 1, 10000)?, - budget: Duration::from_secs(number("budget-seconds", "30", 1, 3600)? as u64), + budget: match number("budget-seconds", "30", 0, 3600)? { + 0 => None, + seconds => Some(Duration::from_secs(seconds as u64)), + }, complex, orientation: single("orientation", "all", &["horizontal", "vertical"])?, outlier, @@ -288,6 +291,15 @@ mod checks { } #[test] fn limits_and_variants() { + assert!(p(&["--budget-seconds", "0"]) + .unwrap() + .unwrap() + .budget + .is_none()); + assert_eq!( + p(&[]).unwrap().unwrap().budget, + Some(Duration::from_secs(30)) + ); assert!(p(&["--test", "14"]).is_err()); assert!(p(&["--test"]).is_err()); assert!(p(&["--count", "0"]).is_err()); diff --git a/performance/rust_app/src/main.rs b/performance/rust_app/src/main.rs index e84c1930..38fc8227 100644 --- a/performance/rust_app/src/main.rs +++ b/performance/rust_app/src/main.rs @@ -10,7 +10,7 @@ fn execute(config: Config) -> Result<(), String> { if cfg!(debug_assertions) { return Err("use cargo run --release".into()); } - let deadline = Instant::now() + config.budget; + let deadline = config.budget.map(|budget| Instant::now() + budget); let stdout = io::stdout(); let mut out = io::BufWriter::new(stdout.lock()); writeln!(out, "{}", test::runner::HEADER).map_err(|e| e.to_string())?; @@ -28,7 +28,7 @@ fn execute(config: Config) -> Result<(), String> { )); } for engine in &config.engines { - if Instant::now() >= deadline { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { eprintln!("budget reached; remaining combinations omitted"); return Ok(()); } diff --git a/performance/rust_app/src/test/runner.rs b/performance/rust_app/src/test/runner.rs index be3c98c2..158a5610 100644 --- a/performance/rust_app/src/test/runner.rs +++ b/performance/rust_app/src/test/runner.rs @@ -150,7 +150,7 @@ pub fn run( variant: &str, size: usize, f: &Fixture, - deadline: Instant, + deadline: Option, out: &mut impl Write, ) -> Result { let case = super::NAMES[id]; @@ -172,7 +172,7 @@ pub fn run( .overlay(f.rule, FillRule::NonZero), ); for strategy in &config.strategies { - if Instant::now() >= deadline { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { return Ok(false); } let s = solver(strategy, config.multithreading); @@ -222,14 +222,14 @@ pub fn run( } }; for _ in 0..config.warmup { - if Instant::now() >= deadline { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { return Ok(false); } operation(); } let mut samples = Vec::with_capacity(config.samples); for _ in 0..config.samples { - if Instant::now() >= deadline { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { return Ok(false); } let start = Instant::now(); From 2221d5e9ffcb4b7ec6ea189aeebb2c8428813b9e Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Mon, 12 Oct 2026 12:53:56 +0300 Subject: [PATCH 3/7] early out with y_range check --- iOverlay/src/split/solver_tree.rs | 7 ++++- iOverlay/tests/tree_y_range_tests.rs | 45 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 iOverlay/tests/tree_y_range_tests.rs diff --git a/iOverlay/src/split/solver_tree.rs b/iOverlay/src/split/solver_tree.rs index 57f923d7..578b0248 100644 --- a/iOverlay/src/split/solver_tree.rs +++ b/iOverlay/src/split/solver_tree.rs @@ -64,8 +64,13 @@ where for (i, si) in segments.iter().enumerate() { let time = si.x_segment.a.x; - let si_range = si.x_segment.y_range().into(); + let si_y_range = si.x_segment.y_range(); + let si_range = si_y_range.clone().into(); for sj in tree.iter_by_range(si_range, time) { + if sj.x_segment.is_not_intersect_y_range(&si_y_range) { + continue; + } + let (this_index, scan_index, this, scan) = if si.x_segment < sj.x_segment { (i, sj.id, &si.x_segment, &sj.x_segment) } else { diff --git a/iOverlay/tests/tree_y_range_tests.rs b/iOverlay/tests/tree_y_range_tests.rs new file mode 100644 index 00000000..70d2c625 --- /dev/null +++ b/iOverlay/tests/tree_y_range_tests.rs @@ -0,0 +1,45 @@ +use i_float::int::point::IntPoint; +use i_overlay::core::fill_rule::FillRule; +use i_overlay::core::integer::OverlayInt; +use i_overlay::core::overlay::Overlay; +use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; + +fn check_tree_y_ranges + std::fmt::Debug>() { + let point = |x: i32, y: i32| IntPoint::::new(x.into(), y.into()); + let rect = |x0, y0, x1, y1| vec![point(x0, y0), point(x0, y1), point(x1, y1), point(x1, y0)]; + // The distant contour puts all nearby, disjoint Y bands in one + // coarse tree cell, while their X ranges overlap. + let mut subj = vec![rect(0, 0, 40, 10), rect(0, 30, 40, 40)]; + let mut clip = vec![rect(10, 10, 30, 20), rect(40, 2, 50, 8)]; + for y in (60..300).step_by(20) { + subj.push(rect(0, y, 40, y + 4)); + clip.push(rect(10, y + 8, 30, y + 12)); + } + subj.push(rect(0, 1_000_000, 40, 1_000_010)); + // Partial horizontal and vertical shared edges above require + // splitting at boundary Y values. These oblique crossings also + // exercise rounding and subsequent split passes. + clip.push(vec![point(-3, 29), point(43, 37), point(17, 43)]); + + for fill in [FillRule::EvenOdd, FillRule::NonZero] { + for rule in [OverlayRule::Union, OverlayRule::Intersect, OverlayRule::Xor] { + let list = Overlay::from_subj_and_clip_custom(&subj, &clip, Default::default(), Solver::LIST) + .overlay(rule, fill); + let tree = Overlay::from_subj_and_clip_custom(&subj, &clip, Default::default(), Solver::TREE) + .overlay(rule, fill); + assert!(!list.is_empty()); + assert_eq!(tree, list, "{rule:?}, {fill:?}"); + } + } +} + +#[test] +fn tree_y_ranges_i32() { + check_tree_y_ranges::(); +} + +#[test] +fn tree_y_ranges_i64() { + check_tree_y_ranges::(); +} From ad1ba9a69a80b7c92d6b409b57a660942f84a546 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Mon, 12 Oct 2026 13:43:07 +0300 Subject: [PATCH 4/7] fix pre allocation memory size --- iOverlay/src/split/grid_layout.rs | 59 +++++++++++++++++++++- iOverlay/src/split/solver_fragment.rs | 72 +++++++++++++++++++++++++-- 2 files changed, 126 insertions(+), 5 deletions(-) diff --git a/iOverlay/src/split/grid_layout.rs b/iOverlay/src/split/grid_layout.rs index 01813f12..e7999c88 100644 --- a/iOverlay/src/split/grid_layout.rs +++ b/iOverlay/src/split/grid_layout.rs @@ -40,7 +40,7 @@ impl FragmentBuffer { let i0 = self.layout.index(s.a.x); if s.a.x < s.b.x { let i1 = self.layout.index(s.b.x - I::ONE); - for count in counts.iter_mut().take(i1).skip(i0) { + for count in counts.iter_mut().take(i1 + 1).skip(i0) { *count += 1; } } else { @@ -304,11 +304,68 @@ mod tests { use crate::geom::x_segment::XSegment; use crate::split::grid_layout::vec; use crate::split::grid_layout::{FragmentBuffer, GridLayout}; + use alloc::vec::Vec; use i_float::int::point::IntPoint; use i_float::int::rect::IntRect; use i_float::triangle::Triangle; use rand::RngExt; + fn check_fragment_reservation() { + // Column width is two. Include one column, several columns, exact + // endpoints, both slopes, and verticals on and off a border. + for (edge, bins, border) in [ + ([0, 1, 1, 1], vec![0], false), + ([1, 1, 2, 1], vec![0], false), + ([1, 0, 2, 3], vec![0], false), + ([0, 1, 8, 1], vec![0, 1, 2, 3], false), + ([1, 0, 9, 5], vec![0, 1, 2, 3, 4], false), + ([1, 5, 9, 0], vec![0, 1, 2, 3, 4], false), + ([0, 0, 0, 3], vec![0], false), + ([3, 0, 3, 3], vec![1], false), + ([4, 0, 4, 3], vec![2], true), + ([12, 0, 12, 3], vec![6], true), + ] { + let [ax, ay, bx, by] = edge.map(I::from_usize); + let segment = XSegment { + a: IntPoint::new(ax, ay), + b: IntPoint::new(bx, by), + }; + let mut buffer = FragmentBuffer::new(GridLayout { + min_x: I::ZERO, + max_x: I::from_usize(12), + power: 1, + }); + for count in [37, 113] { + buffer.init_fragment_buffer(core::iter::repeat_n(segment, count)); + let capacities: Vec<_> = buffer.groups.iter().map(Vec::capacity).collect(); + for &bin in &bins { + assert!(capacities[bin] >= count); + } + for index in 0..count { + buffer.add_segment(index, segment); + } + for (bin, group) in buffer.groups.iter().enumerate() { + assert_eq!(group.len(), if bins.contains(&bin) { count } else { 0 }); + assert_eq!(group.capacity(), capacities[bin]); + } + assert_eq!(buffer.on_border.len(), if border { count } else { 0 }); + buffer.clear(); + assert!(buffer.on_border.is_empty()); + assert!(buffer.groups.iter().all(Vec::is_empty)); + } + } + } + + #[test] + fn fragment_reservation_i32() { + check_fragment_reservation::(); + } + + #[test] + fn fragment_reservation_i64() { + check_fragment_reservation::(); + } + #[test] fn i64_column_positions_with_large_shift() { let layout = GridLayout { diff --git a/iOverlay/src/split/solver_fragment.rs b/iOverlay/src/split/solver_fragment.rs index 266afbee..46be0599 100644 --- a/iOverlay/src/split/solver_fragment.rs +++ b/iOverlay/src/split/solver_fragment.rs @@ -144,10 +144,10 @@ where return false; } - if self.marks.capacity() < size { - let additional = size - self.marks.capacity(); - self.marks.reserve(additional); - } + self.marks.reserve(size); + + #[cfg(test)] + assert!(self.marks.capacity() - self.marks.len() >= size); for mut result in results.into_iter() { self.marks.append(&mut result.marks); @@ -259,6 +259,70 @@ mod tests { use alloc::vec::Vec; use i_float::int::point::IntPoint; + #[cfg(feature = "allow_multithreading")] + fn check_parallel_mark_merge() { + use crate::split::grid_layout::FragmentBuffer; + use i_float::int::number::uint::UIntNumber; + + let mut splitter = SplitSolver::::new(); + splitter.marks.reserve(64); + // Derive the input from the actual capacity, without assuming the + // allocator gives exactly the requested amount. The merge must reserve + // all marks before append, also when a reused empty Vec is too small. + let count = splitter.marks.capacity() * 3 / 4; + for count in [count, count * 3] { + splitter.marks.clear(); + let mut edges = Vec::new(); + for y in 1..=count { + edges.push(XSegment { + a: IntPoint::new(I::ZERO, I::from_usize(y)), + b: IntPoint::new(I::from_usize(100), I::from_usize(y)), + }); + } + edges.push(XSegment { + a: IntPoint::new(I::from_usize(25), I::ZERO), + b: IntPoint::new(I::from_usize(25), I::from_usize(count + 1)), + }); + let mut buffer = + FragmentBuffer::new(GridLayout::new(edges.iter().copied(), edges.len()).unwrap()); + buffer.init_fragment_buffer(edges.iter().copied()); + for (index, edge) in edges.into_iter().enumerate() { + buffer.add_segment(index, edge); + } + splitter.parallel_split(I::WideUInt::ONE, &mut buffer); + assert_eq!(splitter.marks.len(), 2 * count); + splitter + .marks + .sort_unstable_by_key(|mark| (mark.index, mark.point)); + for (index, mark) in splitter.marks[..count].iter().enumerate() { + assert_eq!(mark.index, index); + assert_eq!( + mark.point, + IntPoint::new(I::from_usize(25), I::from_usize(index + 1)) + ); + } + for (index, mark) in splitter.marks[count..].iter().enumerate() { + assert_eq!(mark.index, count); + assert_eq!( + mark.point, + IntPoint::new(I::from_usize(25), I::from_usize(index + 1)) + ); + } + } + } + + #[cfg(feature = "allow_multithreading")] + #[test] + fn parallel_mark_merge_i32() { + check_parallel_mark_merge::(); + } + + #[cfg(feature = "allow_multithreading")] + #[test] + fn parallel_mark_merge_i64() { + check_parallel_mark_merge::(); + } + #[test] fn collinear_overlap_starts_in_a_later_column() { for (start, end, far_end) in [(80, 100, 150), (180, 300, 400)] { From 4f2d77b21ca39c824b244b3ef9628c6b416920c9 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Mon, 12 Oct 2026 20:43:54 +0300 Subject: [PATCH 5/7] clean --- .gitignore | 2 ++ iOverlay/Cargo.toml | 5 ++--- performance/rust_app/Cargo.lock | 4 ++-- performance/rust_app/VALIDATION.md | 18 ------------------ 4 files changed, 6 insertions(+), 23 deletions(-) delete mode 100644 performance/rust_app/VALIDATION.md diff --git a/.gitignore b/.gitignore index f8b85dbf..39f77076 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ debug/ target/ +/AGENTS.md + # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock diff --git a/iOverlay/Cargo.toml b/iOverlay/Cargo.toml index 696ea793..9234f115 100644 --- a/iOverlay/Cargo.toml +++ b/iOverlay/Cargo.toml @@ -12,15 +12,14 @@ readme = "README.md" categories = ["algorithms", "graphics", "science::geo", "mathematics", "no-std"] [dependencies] -i_tree = { version = "^0.19.0" } -i_key_sort = { version = "^0.11.0" } +i_tree = { version = "^0.19" } +i_key_sort = { version = "^0.11" } i_float = { version = "^5.0.0"} i_shape = { version = "^5.0.0"} #i_float = { path = "../../iFloat"} #i_shape = { path = "../../iShape"} -#i_tree = { path = "../../iTree" } #i_key_sort = { path = "../../iKeySort" } rayon = { optional = true, version = "^1.10" } diff --git a/performance/rust_app/Cargo.lock b/performance/rust_app/Cargo.lock index 97870ea3..743472b6 100644 --- a/performance/rust_app/Cargo.lock +++ b/performance/rust_app/Cargo.lock @@ -73,9 +73,9 @@ dependencies = [ [[package]] name = "i_tree" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9d4a992a9fe83130f41ceacceac3bb116a4355dfc9c8d6ecea4f1e4b4c6caf" +checksum = "180cbc5c4af5d8f57b8096c3cf39efdac3642ece00ca54710db44db3536baed4" [[package]] name = "libm" diff --git a/performance/rust_app/VALIDATION.md b/performance/rust_app/VALIDATION.md deleted file mode 100644 index b3c5cb3e..00000000 --- a/performance/rust_app/VALIDATION.md +++ /dev/null @@ -1,18 +0,0 @@ -# Проверка единого numbered-стенда 0.2.0 - -Проверено 12 октября 2026 года. Локальная iOverlay 9.0.1, revision `7a9e659a0355af272cb26383be169557a78e5ea4`; библиотека не изменена. Rust `1.99.0 (b940084d7 2026-09-28)`, target `aarch64-apple-darwin`, macOS. - -- Прежние файлы и номера 0..8 сохранены. Исходные integer-генераторы закреплены golden fingerprints; исходные float-генераторы сохранены с предварительной конвертацией для выбранного integer engine. -- Новые файлы: test_9_orientation, test_10_diagonal, test_11_collinear, test_12_cluster, test_13_seeded. Дублирование старых типов нагрузки удалено. Linear-layout — вариант test_1; integer-спираль — вариант test_3. -- Единственный основной бинарник принимает старые --test/--count/--complex/--int; legacy и отдельный named-case runner удалены. -- `cargo test --release --offline --locked --all-features`: 9 тестов прошли. Все номера и варианты на размерах 1/4/8, все три integer engine и четыре стратегии, reinit, аналитические площади, поворот, coordinate budget, PRNG-векторы, seeded/старые integer fingerprints проверены. -- `cargo clippy --release --offline --locked --all-targets --all-features -- -D warnings`: без предупреждений. -- `results/smoke-numbered.csv`: 384 строки — 16 вариантов номеров 0..13 × 3 engines × 4 strategies × 2 modes. count=8, seed=0, warmup=1, samples=3, iterations=1. -- `results/smoke-numbered-mt.csv`: те же 384 комбинации, включённая feature multithreading, runtime=true, RAYON_NUM_THREADS=4. Все проверки прошли; input fingerprints и сводки результатов совпали с serial-прогоном. Малые размеры не доказывают фактического parallel sort или ускорения. -- `results/baseline.csv`: новый baseline schema=2/fixture_version=2, 128 строк; default count=8, int=i32, warmup=2, samples=7, iterations=3. -- `results/thresholds.csv`: 20 строк, test_1 layout=line, 3992/4000/8000/16000/16008 исходных рёбер, i32, четыре стратегии, cold; результаты совпали. При 16008 route hint запрошенных Tree и Frag одинаков: Frag. -- `results/variants.csv`: 48 строк, test_1 layout=line и test_3 spiral=integer, count=32, все engines/strategies/modes; проверки прошли. -- CSV прочитаны Python csv.DictReader: ожидаемое число строк, schema/fixture_version, структура полей, validated=true, число выборок, медиана/min/max проверены. -- `git diff --check`: без ошибок. Все изменения ограничены performance/rust_app; examples и библиотека не изменены. Commit/push не выполнялись. - -Старые результаты/описания предыдущей организации сохранены в локальном `results/previous-layout/` и не являются baseline новой матрицы. Каталог results игнорируется Git. Команды запуска и ограничения (float, soft budget, диапазон, маршрутизация Tree→Frag) описаны в README. From f4f5dabec41d61bb1bc7a7e487bf03e9d98dd978 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Mon, 12 Oct 2026 21:16:18 +0300 Subject: [PATCH 6/7] performance tests update --- performance/rust_app/README.md | 199 ---------- performance/rust_app/quick.py | 188 --------- performance/rust_app/run.sh | 5 +- performance/rust_app/src/main.rs | 367 +++++++++++++++--- performance/rust_app/src/test/runner.rs | 93 +++++ .../rust_app/src/test/test_0_checkerboard.rs | 52 +++ .../rust_app/src/test/test_1_not_overlap.rs | 47 +++ .../rust_app/src/test/test_2_lines_net.rs | 37 ++ .../rust_app/src/test/test_3_spiral.rs | 32 ++ .../rust_app/src/test/test_4_windows.rs | 40 ++ .../src/test/test_5_nested_squares.rs | 35 ++ .../rust_app/src/test/test_6_corrosion.rs | 29 ++ .../rust_app/src/test/test_7_concentric.rs | 28 ++ .../rust_app/src/test/test_8_wind_mill.rs | 49 +++ performance/rust_app/src/test/util.rs | 18 + 15 files changed, 765 insertions(+), 454 deletions(-) delete mode 100644 performance/rust_app/README.md delete mode 100644 performance/rust_app/quick.py diff --git a/performance/rust_app/README.md b/performance/rust_app/README.md deleted file mode 100644 index 8ce598b0..00000000 --- a/performance/rust_app/README.md +++ /dev/null @@ -1,199 +0,0 @@ -# Performance-тесты локальной iOverlay - -Стенд 0.2.0 подключает локальную iOverlay 9.0.1 через `../../iOverlay`. Прежний подход сохранён: один сценарий — один `src/test/test_N_name.rs`, запуск по номеру через **`--test N`**. Номера 0..8 и исходная геометрия этих тестов сохранены. Отдельного legacy-бинарника нет. - -## Набор тестов - -| Номер | Файл в src/test | Нагрузка / варианты | -|---:|---|---| -| 0 | test_0_checkerboard.rs | Исходная сетка перекрывающихся квадратов, Xor | -| 1 | test_1_not_overlap.rs | Исходная сетка разнесённых квадратов, Union; `--layout line` размещает квадраты вдоль X с O(1) активных рёбер | -| 2 | test_2_lines_net.rs | Исходные горизонтальные/вертикальные полосы, плотная сетка пересечений, Intersect | -| 3 | test_3_spiral.rs | Исходная float-спираль, Subject; `--spiral integer` — детерминированная ортогональная лента с центральным вырезом, Difference | -| 4 | test_4_windows.rs | Исходные окна/отверстия, Difference | -| 5 | test_5_nested_squares.rs | Исходные концентрические прямоугольные рамки, Xor | -| 6 | test_6_corrosion.rs | Исходные сетки многоугольников разных размеров, Difference | -| 7 | test_7_concentric.rs | Исходные концентрические многоугольники с многочисленными клипами, Intersect | -| 8 | test_8_wind_mill.rs | Исходная сетка «мельниц», Intersect | -| **9** | **test_9_orientation.rs** | Длинные перекрывающиеся полосы; `--orientation horizontal/vertical/all`, точный поворот на 90°, Xor | -| **10** | **test_10_diagonal.rs** | Длинные почти параллельные диагональные полосы, мало пересечений, Intersect | -| **11** | **test_11_collinear.rs** | Частичные коллинеарные наложения и общие границы, Xor | -| **12** | **test_12_cluster.rs** | Плотный seeded-кластер; `--outlier false/true/all` перемещает один квадрат внутри кластера или далеко от него, Union | -| **13** | **test_13_seeded.rs** | Детерминированные псевдослучайные прямоугольники, Xor | - -Отдельные новые checkerboard, disjoint, grid, nested, windows убраны: соответствующие нагрузки уже представлены номерами 0,1,2,5/7,4. Целочисленная спираль и линейное размещение разнесённых фигур стали вариантами существующих номеров 3 и 1. Пара направлений находится в одном test_9; кластер и кластер с выбросом — в одном test_12. - -В каждом `test_N_…` находится структура теста и генератор `geometry`. `test/mod.rs` связывает номера с этими генераторами. `test/runner.rs` содержит общий протокол проверки/измерения, `test/data.rs` — представление входа, PRNG, отпечаток и преобразование координат. `config.rs` разбирает параметры, `main.rs` запускает выбранные номера. Геометрия сценариев не спрятана в общем файле generators/fixtures. - -## Запуск - -Из каталога `/Users/nailsharipov/Projects/Rust/iShape/iOverlay/performance/rust_app`: - -```bash -# Прежний способ выбора теста и размера; дополнительные опции необязательны. -./run.sh --test 0 --count 32 --complex false --multithreading false --int all - -# Парное измерение направлений одной геометрии. -./run.sh --test 9 --count 128 --orientation all --strategy all - -# Дополнительные нагрузки; один seed на весь выбранный набор. -./run.sh --test 10,11,12,13 --count 32 --seed 42 - -# Варианты существующих тестов. -./run.sh --test 1 --count 512 --layout line -./run.sh --test 3 --count 128 --spiral integer - -# Полный базовый набор. -mkdir -p results -./run.sh --label my-machine > results/baseline.csv - -# Только геометрия, независимая от sin/cos и float-адаптера. -./run.sh --test 0,1,2,3,4,5,8,9,10,11,12,13 --spiral integer > results/integer.csv - -# Серия размеров в стиле старого complex-режима, с ограничением времени. -./run.sh --test 9,10,11,12,13 --complex true --budget-seconds 30 - -# Пороговые размеры: 3992,4000,8000,16000,16008 исходных рёбер. -./run.sh --test 1 --layout line --count 499,500,1000,2000,2001 \ - --mode cold --warmup 1 --samples 3 --iterations 1 > results/thresholds.csv - -# Потоки требуют явного включения feature. -RAYON_NUM_THREADS=4 cargo run --release --locked --features multithreading -- \ - --test 1 --layout line --count 4096 --multithreading true --strategy auto,tree,frag - -cargo test --release --locked --all-features -cargo clippy --release --locked --all-targets --all-features -- -D warnings -./run.sh --list -./run.sh --help -``` - -Без параметров запускаются 0..13, count=8, seed=0, int=i32, все четыре стратегии, оба режима, без потоков. test_9 и test_12 по умолчанию выполняют оба варианта. Поэтому полный default-прогон содержит **128 строк** CSV; с `--int all` — **384**. По умолчанию test_1 использует старую grid-геометрию, test_3 — старую float-спираль. `--layout all` и `--spiral all` включают оба варианта соответствующего номера. - -`--test`, `--count`, `--strategy`, `--int`, `--mode` принимают списки через запятую или `all` там, где это указано в help. Для integer engine также поддерживаются старые обозначения 16/32/64. `--size` — алиас count, `--extended` — алиас complex=true. `--case` и legacy-feature удалены; пользуйтесь номерами. - -complex=true, как прежде, выбирает серию размеров вместо count; теперь это ограниченная базовая серия 2,4,8,16,32,64. Квадратичные тесты 0,1(grid),2,4,6,8 ограничены count<=64, более дорогой 7 — count<=16; остальные — <=4096. В complex слишком большие комбинации пропускаются с сообщением stderr; при явном count сверх лимита возвращается ошибка. Масштабные старые complex-серии не запускаются автоматически. - -## Измерение и проверка - -Общий протокол применяется к старым и новым номерам: FillRule::NonZero, Precision::HIGH, стандартные integer output options. По умолчанию 2 warm-up операции и 7 выборок по 3 операции; параметры `--warmup`, `--samples`, `--iterations` изменяют это. samples допускает 3..101. Debug-замеры запрещены. - -- **cold**: создание Overlay, загрузка готового входа, Boolean operation, освобождение результата и Overlay. -- **reuse**: восстановление исходных контуров через `reinit_with_subj_and_clip`, Boolean operation, освобождение результата; ёмкости Overlay сохраняются. - -Генерация, преобразование координат, проверка корректности, canonicalization, отпечатки и вывод находятся вне таймера. Изменённые splitter-ом сегменты не переиспользуются как исходный вход. Используется black_box. Cold обозначает создание экземпляра, а не гарантированно холодный CPU/allocator cache. - -Для исходных float-тестов 3/6/7 геометрия сохранена, а преобразование через тот же conservative FloatPointAdapter выполняется заранее с выбранным integer engine. Измеряется **integer-ядро Overlay**, без float-конвертации и float-очистки выходных контуров. Новые времена не сопоставимы напрямую со старыми опубликованными цифрами FloatOverlay. Float-входы используют sin/cos; межплатформенная побитовая стабильность не обещается, итоговая integer-геометрия может различаться между движками. Её реальный отпечаток фиксируется в CSV. Для строго стабильной спирали выбирайте `--spiral integer`. - -Перед замером каждый результат сравнивается с List-эталоном и проверяется reinit. Сравниваются ориентированные выходные границы с независимостью от порядка фигур/контуров и циклической начальной вершины. Направления контуров сохраняются, отверстия учитываются. Это строгий компаратор границ для данного корпуса; расхождения не скрываются допуском. Для test_9 проверяется точное совпадение геометрии после поворота на 90°; превосходство horizontal не предполагается. Все пересечения этой пары целочисленные. - -Мягкий `--budget-seconds` по умолчанию 30; значение **0 полностью отключает бюджет**; сборка не входит в бюджет. Проверки выполняются между комбинациями, warm-up операциями и выборками; одна операция/проверка корректности не прерывается. При исчерпании бюджета незавершённая строка и остальные комбинации пропускаются, stderr сообщает причину, завершённые строки сохраняются (exit 0). Проверяйте полноту CSV. - -## Воспроизводимость и результаты - -**CSV schema=2, fixture_version=2**: добавлены поля `test` и `variant`, изменён корпус. Прежний baseline schema=1 нельзя считать baseline новой матрицы; он сохранён локально в `results/previous-layout/`. Каталог results игнорируется Git. `Cargo.lock` фиксируется для этого executable. - -CSV содержит параметры, фактическое число исходных рёбер input_edges, число контуров, bounding box, input fingerprint, число выходных контуров/рёбер, ориентированную удвоенную площадь и validated=true. Времена — целые наносекунды одной операции, усреднённые внутри выборки. Сохраняются медиана, min/max и все выборки по порядку. Для чётного количества выборок медиана округляется вниз. stdout — только CSV, сообщения — stderr. - -Метаданные включают библиотечную версию/revision/dirty, source fingerprints библиотеки и runner (включая lock), время сборки, Rust/target/profile/opt-level/debug-info/RUSTFLAGS, ОС/доступные CPU, label машины, время запуска, feature потоков и RAYON_NUM_THREADS. Git-поля могут быть unknown вне Git. Для аппаратного сравнения задавайте label с моделью CPU и закрепляйте число потоков. После изменений библиотеки запускайте через Cargo, чтобы пересобрать бинарник. - -SplitMix64 в data.rs использует собственный u64 state и wrapping-арифметику modulo 2^64: - -```text -state += 0x9e3779b97f4a7c15 -z = (state ^ (state >> 30)) * 0xbf58476d1ce4e5b9 -z = (z ^ (z >> 27)) * 0x94d049bb133111eb -return z ^ (z >> 31) -``` - -Первые пять значений seed=0: `e220a8397b1dcdaf,6e789e6aa1b965f4,06c45d188009454f,f88bb8a8724c81ec,1b39896a51a8749b`. - -test_13 генерирует 2n прямоугольников, попеременно Subject и Clip. Четыре значения на прямоугольник: x=r0%4096-2048, y=r1%4096-2048, w=8+r2%256, h=8+r3%256. Порядок вершин: (x,y),(x+w,y),(x+w,y+h),(x,y+h). test_12 использует два значения на пару: x=r0%512-256, y=r1%512-256; Subject 64×64, Clip того же размера со сдвигом (16,16); дополнительный квадрат 8×8 в (0,0) или (12000,12000). Modulo bias намеренно фиксирован. - -Input fingerprint — FNV-1a 64, offset cbf29ce484222325, prime 100000001b3, wrapping-умножение. Для Subject, затем Clip хэшируются little-endian u64: число контуров, длина каждого и пары координат i64 в двоичном дополнении. Порядок входа значим; rule/version — отдельные поля. Это диагностический хэш; StdRng/DefaultHasher не используются. - -Golden test_13 при count=8: seed=0 → 77ac91e43630115d, seed=42 → 95eba339e7c58a49, seed=u64::MAX → b3bbd963a77647f5. В cargo tests закреплены также отпечатки старых integer-генераторов, новых нагрузок и integer-спирали. При намеренном изменении входа обновляйте fixture_version и контрольные векторы. - -## Ограничения - -Каждая integer-координата проверяется до сужения типа: i16 [-16384,16383], i32 [-1073741824,1073741823], i64 [-4611686018427387904,4611686018427387903]. Не подходящие для двигателя комбинации пропускаются с сообщением stderr. Integer-fixtures не масштабируются под двигатель. - -В текущей библиотеке Auto split: List при <4000 сегментов, Tree при 4000..=16000, Frag при >16000. Запрошенный Tree также переходит в Frag при >16000. Fill Auto имеет отдельный порог <8000 после splitting; Tree/Frag используют tree-fill. В CSV strategy — запрос, split_route_hint — оценка по исходным рёбрам; нормализация, слияние и повторные проходы могут изменить сегменты. Fill route также не инструментирован. Tree/Frag на больших данных нельзя выдавать за сравнение разных split-алгоритмов. Библиотека не изменена. - -Малые smoke-данные обычно не достигают parallel-sort порога 32768; multithreading=true не доказывает фактическое распараллеливание. Test_10 содержит почти параллельные полосы с частичным перекрытием и без плотной сетки пересечений; плотные пересечения уже покрывает test_2. Строгие тесты используют размеры 1/4/8; большие или иные float-данные могут выявить новые расхождения — тогда runner сообщит ошибку. - - -## Короткий профиль для сравнения изменений - -```bash -# Выполнять из performance/rust_app. Требуется Python 3. -python3 quick.py --output results/base.csv -# После изменения библиотеки — тот же фиксированный набор: -python3 quick.py --output results/experiment.csv --compare results/base.csv -``` - -`quick-v1` последовательно запускает номера 0..13 с заранее подобранными размерами. -Два движка i32/i64, четыре стратегии, cold/reuse, seed=42; 2 warm-up и 7 выборок -по одной операции. Итого **304 строки**. Потоки выключены. Сборка release выполняется -один раз перед набором; зависимости берутся offline из Cargo.lock. - -| Тест | count | Варианты | -|---:|---|---| -| 0 | 24 | checkerboard | -| 1 | 499,500,2001 | line: 3992/4000/16008 исходных рёбер | -| 2 | 48 | lines_net | -| 3 | 256 | float и integer | -| 4 | 32 | windows | -| 5 | 192 | nested_squares | -| 6 | 10 | corrosion | -| 7 | 8 | concentric | -| 8 | 18 | wind_mill | -| 9 | 256 | horizontal и vertical | -| 10 | 160 | diagonal | -| 11 | 1024 | collinear | -| 12 | 128 | cluster и outlier | -| 13 | 512 | seeded | - -Цель подбора: базовая версия на машине измерения выполняет **весь номер теста** -(оба движка, все стратегии, режимы, размеры и варианты, вместе с генерацией, -проверками и прогревом) быстрее 0,5 с. Это характеристика измеренного baseline, -**не тайм-аут и не гарантия на любой машине**. Размеры никогда не подстраиваются -автоматически. Изменение, которое замедлит тест, будет полностью измерено; -строки не отбрасываются. Профиль передаёт `--budget-seconds 0`. -Сборка и первый запуск свежего бинарника (включая возможную проверку ОС) -учитываются отдельно; перед набором выполняется `--help`. - -Рядом с CSV сохраняется JSON: команды, фиксированный профиль, SHA-256 quick.py, -модель CPU, объём памяти, revision, время сборки/первого запуска, полное время -каждого номера, полнота результата и stderr. Существующие файлы не перезаписываются. -CSV сохраняет все исходные выборки и metadata Rust-стенда. При ошибке остаётся -JSON с complete=false; такой запуск не является baseline. - -`--compare` требует одинаковые ключи, input fingerprint, выходные сводки, -runner_source, компилятор/target, параметры сборки и протокол измерений. -Меняться может исходник библиотеки. Различия в структуре контура или выходной -сводке требуют отдельной проверки; молча сравнивать их нельзя. Итоговый -`.comparison.csv` содержит baseline_ns/current_ns, change_percent и speedup. -Положительный change_percent означает замедление. Сравнивайте на одной машине -с одинаковой нагрузкой и энергорежимом; небольшие различия проверяйте повторным -запуском. Профиль не покрывает i16, многопоточность и весь диапазон больших данных. - - -### Сохранённый baseline quick-v1 - -`results/baselines/2026-10-12-e40116b6/baseline.csv` — Apple M4, 24 GiB, -revision `e40116b6d70157e7393fd2b64b28c01d82049a74`, library_source -`e4461ef685d79b48`, runner_source `579e46a30c4f8a5c`, библиотека без изменений. -304 строки; весь набор 2,879 с, контрольный повтор 2,876 с; максимальный -номер теста в обоих прогонах 0,306 с. Рядом: repeat.csv, comparison, -case-times.csv, summary.json, метаданные запусков и снимок изменений стенда. -Эти результаты локальные: каталог results игнорируется Git. - -```bash -python3 quick.py --output results/experiment.csv \ - --compare results/baselines/2026-10-12-e40116b6/baseline.csv -``` - -В контрольном повторе медиана абсолютного изменения времени составила 1,82%, -p95 — 8,34%, максимум отдельной строки — 34,44%. Это быстрый регрессионный -профиль: единичное небольшое ускорение/замедление не считается доказанным. -Для спорных результатов повторяйте ту же команду с новым именем CSV. diff --git a/performance/rust_app/quick.py b/performance/rust_app/quick.py deleted file mode 100644 index 87ffc6ea..00000000 --- a/performance/rust_app/quick.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 -"""Fixed quick-v1 corpus. Sizes never adapt to the machine or measured speed.""" -import argparse -import csv -import hashlib -import io -import json -import platform -import statistics -import subprocess -import sys -import time -from pathlib import Path - -ROOT = Path(__file__).resolve().parent -PROFILE = [ - (0, "24", []), - (1, "499,500,2001", ["--layout", "line"]), - (2, "48", []), - (3, "256", ["--spiral", "all"]), - (4, "32", []), - (5, "192", []), - (6, "10", []), - (7, "8", []), - (8, "18", []), - (9, "256", ["--orientation", "all"]), - (10, "160", []), - (11, "1024", []), - (12, "128", ["--outlier", "all"]), - (13, "512", []), -] -COMMON = ["--int", "i32,i64", "--strategy", "all", "--mode", "all", - "--seed", "42", "--warmup", "2", "--samples", "7", "--iterations", "1", - "--multithreading", "false", "--budget-seconds", "0"] -KEY = ["test", "variant", "size", "int", "strategy", "mode", "seed"] -CONTRACT = ["schema", "fixture_version", "runner_source", "rustc", "target", - "profile", "opt_level", "debug_info", "rustflags", "mt_compiled", - "multithreading", "precision", "rule", "input_edges", "input_fingerprint", - "output_contours", "output_edges", "output_area2", "warmup", "samples", - "iterations"] - - -def read_rows(text): - reader = csv.DictReader(io.StringIO(text)) - rows = list(reader) - if not rows or not reader.fieldnames: - raise ValueError("empty CSV") - seen = set() - for row in rows: - if None in row or None in row.values() or row["validated"] != "true": - raise ValueError("malformed or unvalidated CSV row") - key = tuple(row[k] for k in KEY) - if key in seen: - raise ValueError(f"duplicate row: {key}") - seen.add(key) - samples = list(map(int, row["samples_ns"].split(";"))) - if (len(samples) != int(row["samples"]) - or int(statistics.median(samples)) != int(row["median_ns"]) - or min(samples) != int(row["min_ns"]) - or max(samples) != int(row["max_ns"])): - raise ValueError(f"invalid sample statistics: {key}") - return reader.fieldnames, rows - - -def compare(before, after): - old = {tuple(r[k] for k in KEY): r for r in before} - new = {tuple(r[k] for k in KEY): r for r in after} - if old.keys() != new.keys(): - raise ValueError("comparison requires exactly the same cases, sizes, engines and modes") - result = [] - for key, row in new.items(): - ref = old[key] - changed = [k for k in CONTRACT if row[k] != ref[k]] - if changed: - raise ValueError(f"incompatible comparison {key}: {', '.join(changed)}") - a, b = int(ref["median_ns"]), int(row["median_ns"]) - if a <= 0 or b <= 0: - raise ValueError(f"nonpositive median: {key}") - result.append({**{k: row[k] for k in KEY}, "baseline_ns": a, "current_ns": b, - "change_percent": f"{100 * (b / a - 1):.3f}", - "speedup": f"{a / b:.4f}"}) - return result - - -def capture(cmd): - result = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True) - return result.stdout.strip() if result.returncode == 0 else "unavailable" - - -def write_csv(path, fields, rows): - with path.open("x", newline="") as file: - writer = csv.DictWriter(file, fieldnames=fields) - writer.writeheader() - writer.writerows(rows) - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--output", required=True, type=Path, help="new CSV; existing files are never overwritten") - parser.add_argument("--compare", type=Path, help="baseline CSV with the same measurement contract") - args = parser.parse_args() - output = args.output.resolve() - meta_path = output.with_suffix(".json") - diff_path = output.with_suffix(".comparison.csv") - for path in [output, meta_path, diff_path]: - if path.exists(): - raise ValueError(f"refusing to overwrite {path}") - before = read_rows(args.compare.read_text())[1] if args.compare else None - output.parent.mkdir(parents=True, exist_ok=True) - build = ["cargo", "build", "--release", "--offline", "--locked", "--bin", "rust_app", - "--no-default-features", "--message-format=json-render-diagnostics"] - start = time.perf_counter() - built = subprocess.run(build, cwd=ROOT, stdout=subprocess.PIPE, text=True) - if built.returncode: - print(built.stdout, file=sys.stderr) - raise RuntimeError("release build failed") - build_seconds = time.perf_counter() - start - artifacts = [json.loads(line) for line in built.stdout.splitlines() if line.startswith("{")] - binary = next(a["executable"] for a in reversed(artifacts) - if a.get("reason") == "compiler-artifact" and a.get("executable") - and a["target"]["name"] == "rust_app") - # First launch of a freshly linked executable can include OS verification. - # Record it separately, before the 14 numbered tests and their warm-ups. - start = time.perf_counter() - subprocess.run([binary, "--help"], check=True, capture_output=True) - launch_seconds = time.perf_counter() - start - host = {"platform": platform.platform(), "machine": platform.machine(), - "cpu": capture(["/usr/sbin/sysctl", "-n", "machdep.cpu.brand_string"]), - "memory_bytes": capture(["/usr/sbin/sysctl", "-n", "hw.memsize"]), - "logical_cpus": capture(["/usr/sbin/sysctl", "-n", "hw.ncpu"])} - label = f"quick-v1 {host['cpu']}" - metadata = {"profile": "quick-v1", "profile_cases": PROFILE, "common_args": COMMON, - "profile_script_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), - "git_revision": capture(["git", "rev-parse", "HEAD"]), "host": host, - "started_unix": time.time(), "build_seconds": build_seconds, - "first_launch_seconds": launch_seconds, "cases": [], "complete": False} - rows, fields = [], None - suite_start = time.perf_counter() - try: - for test, counts, options in PROFILE: - command = [binary, "--test", str(test), "--count", counts, *COMMON, - *options, "--label", label] - start = time.perf_counter() - # Deliberately no timeout, adaptive sizing, or per-case time budget. - run = subprocess.run(command, cwd=ROOT, capture_output=True, text=True) - seconds = time.perf_counter() - start - record = {"test": test, "command": command, "wall_seconds": seconds, - "exit_code": run.returncode, "stderr": run.stderr} - metadata["cases"].append(record) - if run.returncode or run.stderr: - raise RuntimeError(f"test {test} failed or skipped data: {run.stderr}") - header, case_rows = read_rows(run.stdout) - variants = {1: ["line"], 3: ["float", "integer"], - 9: ["horizontal", "vertical"], 12: ["cluster", "outlier"]}.get(test, ["default"]) - expected = {(str(test), v, n, i, s, m, "42") - for v in variants for n in counts.split(",") for i in ["i32", "i64"] - for s in ["auto", "list", "tree", "frag"] for m in ["cold", "reuse"]} - if {tuple(r[k] for k in KEY) for r in case_rows} != expected: - raise ValueError(f"test {test}: incomplete or unexpected matrix") - if fields is not None and fields != header: - raise ValueError("CSV header changed between tests") - fields = header - rows.extend(case_rows) - record["rows"] = len(case_rows) - record["input_edges"] = sorted({int(r["input_edges"]) for r in case_rows}) - print(f"test {test:2}: {seconds:.3f}s, {len(case_rows)} rows" + - (" (above baseline target; retained)" if seconds > 0.5 else ""), file=sys.stderr) - metadata["complete"] = True - finally: - metadata["suite_seconds"] = time.perf_counter() - suite_start - metadata["rows"] = len(rows) - with meta_path.open("x") as file: - json.dump(metadata, file, indent=2) - file.write("\n") - write_csv(output, fields, rows) - if before is not None: - changes = compare(before, rows) - write_csv(diff_path, list(changes[0]), changes) - print(f"Comparison: {diff_path} (positive change_percent = slower)", file=sys.stderr) - print(f"Saved {len(rows)} rows: {output}\nSuite: {metadata['suite_seconds']:.3f}s", file=sys.stderr) - - -if __name__ == "__main__": - try: - main() - except (ValueError, RuntimeError, OSError, subprocess.CalledProcessError) as error: - print(f"error: {error}", file=sys.stderr) - sys.exit(1) diff --git a/performance/rust_app/run.sh b/performance/rust_app/run.sh index 8be59369..1969e792 100755 --- a/performance/rust_app/run.sh +++ b/performance/rust_app/run.sh @@ -1,4 +1,3 @@ #!/bin/bash -set -euo pipefail -cd "$(dirname "$0")" -cargo run --release --locked --bin rust_app -- "$@" + +target/release/rust_app --multithreading false --complex true --test 0 --int all diff --git a/performance/rust_app/src/main.rs b/performance/rust_app/src/main.rs index 38fc8227..0afbf95c 100644 --- a/performance/rust_app/src/main.rs +++ b/performance/rust_app/src/main.rs @@ -1,73 +1,312 @@ +use crate::test::test_0_checkerboard::CheckerboardTest; +use crate::test::test_1_not_overlap::NotOverlapTest; +use crate::test::test_2_lines_net::LinesNetTest; +use crate::test::test_3_spiral::SpiralTest; +use crate::test::test_4_windows::WindowsTest; +use crate::test::test_5_nested_squares::CrossTest; +use crate::test::test_6_corrosion::CorrosionTest; +use crate::test::test_7_concentric::ConcentricTest; +use crate::test::test_8_wind_mill::WindMillTest; +use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::{MultithreadOptions, Precision, Solver, Strategy}; +use std::collections::HashMap; +use std::env; + mod config; mod test; -use config::Config; -use std::{ - io::{self, Write}, - time::Instant, -}; - -fn execute(config: Config) -> Result<(), String> { - if cfg!(debug_assertions) { - return Err("use cargo run --release".into()); - } - let deadline = config.budget.map(|budget| Instant::now() + budget); - let stdout = io::stdout(); - let mut out = io::BufWriter::new(stdout.lock()); - writeln!(out, "{}", test::runner::HEADER).map_err(|e| e.to_string())?; - for &id in &config.tests { - for variant in config.variants(id) { - for &n in &config.counts { - if n > test::max_count(id, variant) { - if config.complex { - eprintln!("skip test {id} {variant} count={n}: size cap"); - continue; - } - return Err(format!( - "test {id} {variant}: count exceeds {}", - test::max_count(id, variant) - )); - } - for engine in &config.engines { - if deadline.is_some_and(|deadline| Instant::now() >= deadline) { - eprintln!("budget reached; remaining combinations omitted"); - return Ok(()); - } - let completed = match engine.as_str() { - "i16" => { - let f = test::generate::(id, n, config.seed, variant)?; - test::runner::run::( - &config, id, variant, n, &f, deadline, &mut out, - )? - } - "i32" => { - let f = test::generate::(id, n, config.seed, variant)?; - test::runner::run::( - &config, id, variant, n, &f, deadline, &mut out, - )? - } - "i64" => { - let f = test::generate::(id, n, config.seed, variant)?; - test::runner::run::( - &config, id, variant, n, &f, deadline, &mut out, - )? - } - _ => unreachable!(), - }; - if !completed { - eprintln!("budget reached; incomplete row omitted"); - return Ok(()); - } + +#[derive(Clone, Copy)] +enum IntEngine { + I16, + I32, + I64, +} + +impl IntEngine { + fn parse(value: &str) -> Vec { + match value { + "i16" | "16" => vec![Self::I16], + "i32" | "32" => vec![Self::I32], + "i64" | "64" => vec![Self::I64], + "all" => vec![Self::I16, Self::I32, Self::I64], + _ => panic!("Unknown int engine: {value}. Use i16, i32, i64, or all"), + } + } + + fn label(self) -> &'static str { + match self { + Self::I16 => "i16", + Self::I32 => "i32", + Self::I64 => "i64", + } + } +} + +fn main() { + let args = env::args(); + let mut args_iter = args.peekable(); + let mut args_map = HashMap::new(); + + while let Some(arg) = args_iter.next() { + if let Some(key) = arg.strip_prefix("--").or_else(|| arg.strip_prefix('-')) { + // If the next argument is also a key, store a boolean flag; otherwise, store the value. + let value = if args_iter.peek().is_some_and(|a| a.starts_with('-')) { + "true".to_string() + } else { + args_iter.next().unwrap_or_else(|| "true".to_string()) + }; + args_map.insert(key.to_owned(), value); + } + } + + #[cfg(debug_assertions)] + { + if args_map.is_empty() { + args_map.insert("multithreading".to_string(), "false".to_string()); + args_map.insert("complex".to_string(), "false".to_string()); + args_map.insert("test".to_string(), 3.to_string()); + args_map.insert("int".to_string(), "i64".to_string()); + let count = 4096; + args_map.insert("count".to_string(), count.to_string()); + } + } + + if args_map + .get("benchmark") + .is_some_and(|value| value == "true") + { + let args = args_map + .iter() + .filter(|(key, _)| key.as_str() != "benchmark") + .flat_map(|(key, value)| [format!("--{key}"), value.clone()]); + let result = + config::parse(args).and_then(|c| c.map(test::runner::run_benchmark).unwrap_or(Ok(()))); + if let Err(error) = result { + eprintln!("error: {error}"); + std::process::exit(2); + } + return; + } + + let test_key = args_map.get("test").expect("Test number is not set"); + let multithreading_key = args_map + .get("multithreading") + .expect("Multithreading is not set"); + let complex_key = args_map.get("complex").expect("Complex is not set"); + let int_key = args_map.get("int").map(String::as_str).unwrap_or("i32"); + + let test: usize = test_key + .parse() + .expect("Unable to parse test as an integer"); + let multithreading: bool = multithreading_key + .parse() + .expect("Unable to parse multithreading as an boolean"); + let complex: bool = complex_key + .parse() + .expect("Unable to parse complex as an boolean"); + let int_engines = IntEngine::parse(int_key); + + let multithreading = if multithreading { + Some(MultithreadOptions::default()) + } else { + None + }; + + let solver = Solver { + strategy: match args_map + .get("strategy") + .map(String::as_str) + .unwrap_or("auto") + { + "auto" => Strategy::Auto, + "list" => Strategy::List, + "tree" => Strategy::Tree, + "frag" => Strategy::Frag, + value => panic!("Unknown strategy: {value}. Use auto, list, tree, or frag"), + }, + precision: Precision::HIGH, + multithreading, + }; + + for int_engine in int_engines { + println!("int engine: {}", int_engine.label()); + run_selected_test(int_engine, test, complex, &args_map, solver); + } +} + +fn run_selected_test( + int_engine: IntEngine, + test: usize, + complex: bool, + args_map: &HashMap, + solver: Solver, +) { + match int_engine { + IntEngine::I16 => run_selected_test_with_int::(test, complex, args_map, solver), + IntEngine::I32 => run_selected_test_with_int::(test, complex, args_map, solver), + IntEngine::I64 => run_selected_test_with_int::(test, complex, args_map, solver), + } +} + +fn run_selected_test_with_int( + test: usize, + complex: bool, + args_map: &HashMap, + solver: Solver, +) { + if complex { + match test { + 0 => { + run_test_0::(solver); + } + 1 => { + run_test_1::(solver); + } + 2 => { + run_test_2::(solver); + } + 3 => { + run_test_3::(solver); + } + 4 => { + run_test_4::(solver); + } + 5 => { + run_test_5::(solver); + } + 6 => { + run_test_6::(solver); + } + 7 => { + run_test_7::(solver); + } + 8 => { + run_test_8::(solver); + } + 9..=13 => { + for count in [2, 4, 8, 16, 32, 64] { + test::runner::run_selected::(test, count, args_map, solver); } } + _ => { + println!("Test is not found"); + } + } + } else { + let count_key = args_map.get("count").expect("Count is not set"); + let count: usize = count_key + .parse() + .expect("Unable to parse count as an integer"); + match test { + 0 => { + CheckerboardTest::run::(count, OverlayRule::Xor, solver, 1.0); + } + 1 => { + NotOverlapTest::run::(count, OverlayRule::Union, solver, 1.0); + } + 2 => { + LinesNetTest::run::(count, OverlayRule::Intersect, solver, 1.0); + } + 3 => { + SpiralTest::run::(count, solver, 100.0); + } + 4 => { + WindowsTest::run::(count, OverlayRule::Difference, solver, 1.0); + } + 5 => { + CrossTest::run::(count, OverlayRule::Xor, solver, 1.0); + } + 6 => { + CorrosionTest::run::(count, OverlayRule::Difference, solver, 1.0); + } + 7 => { + ConcentricTest::run::(count, OverlayRule::Intersect, solver, 1.0); + } + 8 => { + WindMillTest::run::(count, OverlayRule::Intersect, solver, 1.0); + } + 9..=13 => { + test::runner::run_selected::(test, count, args_map, solver); + } + _ => { + println!("Test is not found"); + } } } - out.flush().map_err(|e| e.to_string()) } -fn main() { - let result = - config::parse(std::env::args().skip(1)).and_then(|c| c.map(execute).unwrap_or(Ok(()))); - if let Err(e) = result { - eprintln!("error: {e}"); - std::process::exit(2); + +fn run_test_0(solver: Solver) { + println!("run Checkerboard test"); + for i in 1..11 { + let n = 1 << i; + CheckerboardTest::run::(n, OverlayRule::Xor, solver, 1000.0); + } +} + +fn run_test_1(solver: Solver) { + println!("run NotOverlap test"); + for i in 1..11 { + let n = 1 << i; + NotOverlapTest::run::(n, OverlayRule::Xor, solver, 1000.0); + } +} + +fn run_test_2(solver: Solver) { + println!("run LinesNet test"); + for i in 1..11 { + let n = 1 << i; + LinesNetTest::run::(n, OverlayRule::Intersect, solver, 500.0); + } +} + +fn run_test_3(solver: Solver) { + println!("run Spiral test"); + for i in 1..20 { + let n = 1 << i; + SpiralTest::run::(n, solver, 1000.0) + } +} + +fn run_test_4(solver: Solver) { + println!("run Windows test"); + for i in 1..11 { + let n = 1 << i; + WindowsTest::run::(n, OverlayRule::Difference, solver, 500.0); + } +} + +fn run_test_5(solver: Solver) { + println!("run NestedSquares test"); + for i in 1..17 { + let n = 1 << i; + CrossTest::run::(n, OverlayRule::Xor, solver, 500.0); + } +} + +fn run_test_6(solver: Solver) { + println!("run Corrosion test"); + let mut n = 1; + for _ in 1..11 { + CorrosionTest::run::(n, OverlayRule::Difference, solver, 100.0); + n <<= 1; + } +} + +fn run_test_7(solver: Solver) { + println!("run Concentric test"); + let mut n = 1; + for _ in 1..11 { + ConcentricTest::run::(n, OverlayRule::Intersect, solver, 100.0); + n <<= 1; + } +} + +fn run_test_8(solver: Solver) { + println!("run WindMill test"); + let mut n = 1; + for _ in 1..11 { + WindMillTest::run::(n, OverlayRule::Difference, solver, 100.0); + n <<= 1; } + WindMillTest::validate::(100, OverlayRule::Difference, solver); } diff --git a/performance/rust_app/src/test/runner.rs b/performance/rust_app/src/test/runner.rs index 158a5610..ab7d0908 100644 --- a/performance/rust_app/src/test/runner.rs +++ b/performance/rust_app/src/test/runner.rs @@ -8,6 +8,7 @@ use i_overlay::core::{ }; use i_overlay::i_float::int::point::IntPoint; use i_overlay::i_shape::int::{path::IntPaths, shape::IntShapes}; +use std::collections::HashMap; use std::{ hint::black_box, io::{self, Write}, @@ -322,6 +323,98 @@ pub fn run( Ok(true) } +pub fn run_selected( + id: usize, + count: usize, + args_map: &HashMap, + solver: Solver, +) { + let args = args_map + .iter() + .filter(|(key, _)| { + !["test", "count", "complex", "int", "benchmark", "strategy"].contains(&key.as_str()) + }) + .flat_map(|(key, value)| [format!("--{key}"), value.clone()]) + .chain([ + "--test".into(), + id.to_string(), + "--count".into(), + count.to_string(), + "--complex".into(), + "false".into(), + "--strategy".into(), + format!("{:?}", solver.strategy).to_lowercase(), + ]); + let config = crate::config::parse(args) + .expect("Invalid benchmark options") + .expect("Missing test configuration"); + let deadline = config.budget.map(|budget| Instant::now() + budget); + let stdout = io::stdout(); + let mut out = io::BufWriter::new(stdout.lock()); + writeln!(out, "{}", HEADER).expect("Unable to write CSV header"); + for variant in config.variants(id) { + let fixture = super::generate::(id, count, config.seed, variant) + .expect("Unable to generate test input"); + if !run::(&config, id, variant, count, &fixture, deadline, &mut out) + .expect("Benchmark failed") + { + eprintln!("budget reached; remaining variants omitted"); + break; + } + } +} +pub fn run_benchmark(config: Config) -> Result<(), String> { + if cfg!(debug_assertions) { + return Err("use cargo run --release".into()); + } + let deadline = config.budget.map(|budget| Instant::now() + budget); + let stdout = io::stdout(); + let mut out = io::BufWriter::new(stdout.lock()); + writeln!(out, "{}", HEADER).map_err(|e| e.to_string())?; + for &id in &config.tests { + for variant in config.variants(id) { + for &n in &config.counts { + if n > super::max_count(id, variant) { + if config.complex { + eprintln!("skip test {id} {variant} count={n}: size cap"); + continue; + } + return Err(format!( + "test {id} {variant}: count exceeds {}", + super::max_count(id, variant) + )); + } + for engine in &config.engines { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { + eprintln!("budget reached; remaining combinations omitted"); + return Ok(()); + } + let completed = match engine.as_str() { + "i16" => { + let f = super::generate::(id, n, config.seed, variant)?; + run::(&config, id, variant, n, &f, deadline, &mut out)? + } + "i32" => { + let f = super::generate::(id, n, config.seed, variant)?; + run::(&config, id, variant, n, &f, deadline, &mut out)? + } + "i64" => { + let f = super::generate::(id, n, config.seed, variant)?; + run::(&config, id, variant, n, &f, deadline, &mut out)? + } + _ => unreachable!(), + }; + if !completed { + eprintln!("budget reached; incomplete row omitted"); + return Ok(()); + } + } + } + } + } + out.flush().map_err(|e| e.to_string()) +} + #[cfg(test)] mod checks { use super::*; diff --git a/performance/rust_app/src/test/test_0_checkerboard.rs b/performance/rust_app/src/test/test_0_checkerboard.rs index ecf6cde4..08870cec 100644 --- a/performance/rust_app/src/test/test_0_checkerboard.rs +++ b/performance/rust_app/src/test/test_0_checkerboard.rs @@ -1,7 +1,12 @@ use super::data::Fixture; +use super::util::OverlayInt; use super::util::Util; +use i_overlay::core::fill_rule::FillRule; +use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; use i_overlay::i_float::int::point::IntPoint; +use std::time::Instant; pub(crate) struct CheckerboardTest; /* @@ -95,6 +100,53 @@ multithreading off */ impl CheckerboardTest { + pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { + // 1000 + if Util::skip_if_out_of_range::(n, 30 * n + 20) { + return; + } + + let subj_paths = Util::many_squares( + IntPoint::new(I::ZERO, I::ZERO), + I::from_usize(20), + I::from_usize(30), + n, + ); + let clip_paths = Util::many_squares( + IntPoint::new(I::from_usize(15), I::from_usize(15)), + I::from_usize(20), + I::from_usize(30), + n - 1, + ); + + let it_count = ((scale / (n as f64)) as usize).max(1); + let sq_it_count = it_count * it_count; + + let start = Instant::now(); + + for _i in 0..sq_it_count { + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); + } + + let duration = start.elapsed(); + let time = duration.as_secs_f64() / sq_it_count as f64; + + let polygons_count = n * n + (n - 1) * (n - 1); + let count_log = (polygons_count as f64).log10(); + let time_log = time.log10(); + + println!( + "{}({} {:.1}) - {:.6}({:.1})", + n, polygons_count, count_log, time, time_log + ); + } + pub(crate) fn geometry(n: usize) -> Fixture { let subj = Util::many_squares(IntPoint::new(0i64, 0), 20, 30, n); let clip = Util::many_squares(IntPoint::new(15i64, 15), 20, 30, n - 1); diff --git a/performance/rust_app/src/test/test_1_not_overlap.rs b/performance/rust_app/src/test/test_1_not_overlap.rs index 57e99e92..0ba79d69 100644 --- a/performance/rust_app/src/test/test_1_not_overlap.rs +++ b/performance/rust_app/src/test/test_1_not_overlap.rs @@ -1,7 +1,12 @@ use super::data::Fixture; +use super::util::OverlayInt; use super::util::Util; +use i_overlay::core::fill_rule::FillRule; +use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; use i_overlay::i_float::int::point::IntPoint; +use std::time::Instant; pub(crate) struct NotOverlapTest; /* @@ -100,6 +105,48 @@ multithreading off */ impl NotOverlapTest { + pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { + // 1000 + if Util::skip_if_out_of_range::(n, 30 * n + 10) { + return; + } + + let subj_paths = Util::many_squares( + IntPoint::new(I::ZERO, I::ZERO), + I::from_usize(10), + I::from_usize(30), + n, + ); + let clip_paths = Util::many_squares( + IntPoint::new(I::from_usize(15), I::from_usize(15)), + I::from_usize(10), + I::from_usize(30), + n - 1, + ); + + let it_count = ((scale / (n as f64)) as usize).max(1); + let sq_it_count = it_count * it_count; + + let start = Instant::now(); + + for _i in 0..sq_it_count { + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); + } + + let duration = start.elapsed(); + let time = duration.as_secs_f64() / sq_it_count as f64; + + let polygons_count = n * n + (n - 1) * (n - 1); + + println!("{:.1} - {:.6}", polygons_count, time); + } + pub(crate) fn geometry(n: usize, line: bool) -> Fixture { if line { let mut f = Fixture { diff --git a/performance/rust_app/src/test/test_2_lines_net.rs b/performance/rust_app/src/test/test_2_lines_net.rs index 6e501f5a..aee03b11 100644 --- a/performance/rust_app/src/test/test_2_lines_net.rs +++ b/performance/rust_app/src/test/test_2_lines_net.rs @@ -1,6 +1,11 @@ use super::data::Fixture; +use super::util::OverlayInt; use super::util::Util; +use i_overlay::core::fill_rule::FillRule; +use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; +use std::time::Instant; pub(crate) struct LinesNetTest; /* @@ -99,6 +104,38 @@ multithreading off */ impl LinesNetTest { + pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { + // 500 + if Util::skip_if_out_of_range::(n, 10 * n + 10) { + return; + } + + let subj_paths = Util::many_lines_x(I::from_usize(20), n); + let clip_paths = Util::many_lines_y(I::from_usize(20), n); + + let it_count = ((scale / (n as f64)) as usize).max(1); + let sq_it_count = it_count * it_count; + + let start = Instant::now(); + + for _ in 0..sq_it_count { + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); + } + + let duration = start.elapsed(); + let time = duration.as_secs_f64() / sq_it_count as f64; + + let polygons_count = 2 * n; + + println!("{} - {:.6}", polygons_count, time); + } + pub(crate) fn geometry(n: usize) -> Fixture { Fixture::from_int( Util::many_lines_x(20i64, n), diff --git a/performance/rust_app/src/test/test_3_spiral.rs b/performance/rust_app/src/test/test_3_spiral.rs index e5791858..9b8a8b04 100644 --- a/performance/rust_app/src/test/test_3_spiral.rs +++ b/performance/rust_app/src/test/test_3_spiral.rs @@ -1,7 +1,13 @@ use super::data::Fixture; use super::runner::Engine; +use super::util::OverlayInt; use super::util::Util; +use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; +use i_overlay::float::overlay::FloatOverlay; +use i_overlay::i_float::float::point::FloatPoint; +use std::time::Instant; pub(crate) struct SpiralTest; /* @@ -150,6 +156,32 @@ multithreading off */ impl SpiralTest { + pub(crate) fn run(n: usize, solver: Solver, scale: f64) { + // 1000 + let subj_path = Util::spiral(n, 100.0); + + let it_count = ((scale / (n as f64)) as usize).max(1); + let sq_it_count = it_count * it_count; + + let start = Instant::now(); + + for _ in 0..sq_it_count { + let _ = FloatOverlay::, I>::from_subj_custom( + &subj_path, + Default::default(), + solver, + ) + .overlay(OverlayRule::Subject, FillRule::NonZero); + } + + let duration = start.elapsed(); + let time = duration.as_secs_f64() / sq_it_count as f64; + + let polygons_count = n; + + println!("{} - {:.6}", polygons_count, time); + } + pub(crate) fn geometry(n: usize, integer: bool) -> Fixture { if integer { return Self::integer_geometry(n); diff --git a/performance/rust_app/src/test/test_4_windows.rs b/performance/rust_app/src/test/test_4_windows.rs index 9235d6b6..02b5a2b7 100644 --- a/performance/rust_app/src/test/test_4_windows.rs +++ b/performance/rust_app/src/test/test_4_windows.rs @@ -1,7 +1,12 @@ use super::data::Fixture; +use super::util::OverlayInt; use super::util::Util; +use i_overlay::core::fill_rule::FillRule; +use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; use i_overlay::i_float::int::point::IntPoint; +use std::time::Instant; pub(crate) struct WindowsTest; /* @@ -90,6 +95,41 @@ i64 */ impl WindowsTest { + pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { + // 500 + if Util::skip_if_out_of_range::(n, 15 * n + 20) { + return; + } + + let offset = I::from_usize(30); + let x = I::from_usize(n) * offset / I::TWO; + let origin = IntPoint::new(-x, -x); + let (subj_paths, clip_paths) = + Util::many_windows(origin, I::from_usize(20), I::from_usize(10), offset, n); + + let it_count = ((scale / (n as f64)) as usize).max(1); + let sq_it_count = it_count * it_count; + + let start = Instant::now(); + + for _ in 0..sq_it_count { + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); + } + + let duration = start.elapsed(); + let time = duration.as_secs_f64() / sq_it_count as f64; + + let polygons_count = 2 * n * n; + + println!("{} - {:.6}", polygons_count, time); + } + pub(crate) fn geometry(n: usize) -> Fixture { let x = n as i64 * 30 / 2; let (subj, clip) = Util::many_windows(IntPoint::new(-x, -x), 20, 10, 30, n); diff --git a/performance/rust_app/src/test/test_5_nested_squares.rs b/performance/rust_app/src/test/test_5_nested_squares.rs index e436b0b1..78b80fff 100644 --- a/performance/rust_app/src/test/test_5_nested_squares.rs +++ b/performance/rust_app/src/test/test_5_nested_squares.rs @@ -1,6 +1,11 @@ use super::data::Fixture; +use super::util::OverlayInt; use super::util::Util; +use i_overlay::core::fill_rule::FillRule; +use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; +use std::time::Instant; pub(crate) struct CrossTest; /* @@ -116,6 +121,36 @@ i64 */ impl CrossTest { + pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { + // 500 + if Util::skip_if_out_of_range::(n, 8 * n + 8) { + return; + } + + let (subj_paths, clip_paths) = Util::concentric_squares(I::from_usize(2), n); + + let it_count = ((scale / (n as f64)) as usize).max(1); + let sq_it_count = it_count * it_count; + + let start = Instant::now(); + + for _ in 0..sq_it_count { + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); + } + let duration = start.elapsed(); + let time = duration.as_secs_f64() / sq_it_count as f64; + + let polygons_count = 2 * n; + + println!("{} - {:.6}", polygons_count, time); + } + pub(crate) fn geometry(n: usize) -> Fixture { let (subj, clip) = Util::concentric_squares(2i64, n); Fixture::from_int(subj, clip, OverlayRule::Xor) diff --git a/performance/rust_app/src/test/test_6_corrosion.rs b/performance/rust_app/src/test/test_6_corrosion.rs index 2f714558..85067b27 100644 --- a/performance/rust_app/src/test/test_6_corrosion.rs +++ b/performance/rust_app/src/test/test_6_corrosion.rs @@ -1,7 +1,12 @@ use super::data::Fixture; use super::runner::Engine; +use super::util::OverlayInt; +use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; +use i_overlay::float::overlay::FloatOverlay; use std::f64::consts::PI; +use std::time::Instant; type FloatGeometry = (Vec>, Vec>); pub(crate) struct CorrosionTest; @@ -90,6 +95,30 @@ i64 */ impl CorrosionTest { + pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { + // 500 + let (subj_paths, clip_paths) = Self::float_geometry(100.0, n); + + let it_count = ((scale / (n as f64)) as usize).max(1); + let sq_it_count = it_count * it_count; + + let start = Instant::now(); + + for _ in 0..sq_it_count { + let mut overlay = FloatOverlay::<[f64; 2], I>::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ); + let _res = overlay.overlay(rule, FillRule::NonZero); + } + let duration = start.elapsed(); + let time = duration.as_secs_f64() / sq_it_count as f64; + + println!("{} - {:.6}", n, time); + } + pub(crate) fn geometry(n: usize) -> Fixture { let (subj, clip) = Self::float_geometry(100.0, n); Fixture::from_float::(subj, clip, OverlayRule::Difference) diff --git a/performance/rust_app/src/test/test_7_concentric.rs b/performance/rust_app/src/test/test_7_concentric.rs index bd920c87..38a0d8e8 100644 --- a/performance/rust_app/src/test/test_7_concentric.rs +++ b/performance/rust_app/src/test/test_7_concentric.rs @@ -1,7 +1,12 @@ use super::data::Fixture; use super::runner::Engine; +use super::util::OverlayInt; +use i_overlay::core::fill_rule::FillRule; use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; +use i_overlay::float::overlay::FloatOverlay; use std::f64::consts::PI; +use std::time::Instant; type FloatGeometry = (Vec>, Vec>); pub(crate) struct ConcentricTest; @@ -92,6 +97,29 @@ i64 */ impl ConcentricTest { + pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { + let (subj_paths, clip_paths) = Self::float_geometry(100.0, n); + + let it_count = ((scale / (n as f64)) as usize).max(1); + let sq_it_count = it_count * it_count; + + let start = Instant::now(); + + for _ in 0..sq_it_count { + let mut overlay = FloatOverlay::<[f64; 2], I>::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ); + let _res = overlay.overlay(rule, FillRule::NonZero); + } + let duration = start.elapsed(); + let time = duration.as_secs_f64() / sq_it_count as f64; + + println!("{} - {:.6}", n, time); + } + pub(crate) fn geometry(n: usize) -> Fixture { let (subj, clip) = Self::float_geometry(100.0, n); Fixture::from_float::(subj, clip, OverlayRule::Intersect) diff --git a/performance/rust_app/src/test/test_8_wind_mill.rs b/performance/rust_app/src/test/test_8_wind_mill.rs index 634cb1e8..42330566 100644 --- a/performance/rust_app/src/test/test_8_wind_mill.rs +++ b/performance/rust_app/src/test/test_8_wind_mill.rs @@ -1,8 +1,14 @@ use super::data::Fixture; +use super::util::OverlayInt; +use super::util::Util; +use i_overlay::core::fill_rule::FillRule; +use i_overlay::core::overlay::Overlay; use i_overlay::core::overlay_rule::OverlayRule; +use i_overlay::core::solver::Solver; use i_overlay::i_float::int::number::int::IntNumber; use i_overlay::i_float::int::point::IntPoint; use i_overlay::i_shape::int::shape::IntContour; +use std::time::Instant; pub(crate) struct WindMillTest; /* @@ -89,6 +95,49 @@ i64 */ impl WindMillTest { + pub(crate) fn run(n: usize, rule: OverlayRule, solver: Solver, scale: f64) { + if Util::skip_if_out_of_range::(n, 80 * n + 80) { + return; + } + + let (subj_paths, clip_paths) = Self::int_geometry(I::from_usize(80), n); + + let it_count = ((scale / (n as f64)) as usize).max(1); + let sq_it_count = it_count * it_count; + + let start = Instant::now(); + + for _ in 0..sq_it_count { + let _ = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); + } + + let duration = start.elapsed(); + let time = duration.as_secs_f64() / sq_it_count as f64; + + println!("{} - {:.6}", n, time); + } + + pub(crate) fn validate(n: usize, rule: OverlayRule, solver: Solver) { + let (subj_paths, clip_paths) = Self::int_geometry(I::from_usize(80), n); + + let res = Overlay::from_subj_and_clip_custom( + &subj_paths, + &clip_paths, + Default::default(), + solver, + ) + .overlay(rule, FillRule::NonZero); + + assert_eq!(res.len(), n * n); + println!("result validation PASS"); + } + pub(crate) fn geometry(n: usize) -> Fixture { let (subj, clip) = Self::int_geometry(80i64, n); Fixture::from_int(subj, clip, OverlayRule::Intersect) diff --git a/performance/rust_app/src/test/util.rs b/performance/rust_app/src/test/util.rs index d2acf23c..1c52fd3b 100644 --- a/performance/rust_app/src/test/util.rs +++ b/performance/rust_app/src/test/util.rs @@ -7,7 +7,25 @@ use std::f64::consts::PI; pub(super) struct Util; +pub(crate) use super::runner::Engine as OverlayInt; + impl Util { + pub(super) fn skip_if_out_of_range(n: usize, max_abs_coord: usize) -> bool { + let max = I::MAX.to_usize() / 2; + if max_abs_coord <= max { + return false; + } + + println!( + "{} - skipped (requires coordinate {}, i{} max {})", + n, + max_abs_coord, + I::BITS, + max + ); + true + } + pub(super) fn many_squares( start: IntPoint, size: I, From 7e574567aa59225f434be573343e920d8449d2f1 Mon Sep 17 00:00:00 2001 From: Nail Sharipov Date: Mon, 12 Oct 2026 21:27:39 +0300 Subject: [PATCH 7/7] fix cargo --- performance/rust_app/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/performance/rust_app/Cargo.toml b/performance/rust_app/Cargo.toml index 9469d194..08c0dde2 100644 --- a/performance/rust_app/Cargo.toml +++ b/performance/rust_app/Cargo.toml @@ -15,4 +15,4 @@ default = [] multithreading = ["i_overlay/allow_multithreading"] [dependencies] -i_overlay = { path = "../../iOverlay", version = "9.0.1" } +i_overlay = { path = "../../iOverlay", version = "^9" }