Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,29 @@ jobs:
- name: Run repository contracts
run: python -m unittest discover -s scripts/tests -v

performance-contracts:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Set up stable Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
with:
toolchain: stable

- name: Verify allocation contracts
run: cargo test --test allocation_contract --all-features -- --test-threads=1

- name: Capture shared-backing evidence
shell: bash
run: |
cargo bench --bench shared_backing -- __allocation_evidence_only__ --noplot 2>&1 |
tee target/allocation-evidence.log

- name: Verify shared-backing evidence
run: python scripts/verify-allocation-evidence.py target/allocation-evidence.log
Comment on lines +109 to +130
Comment on lines +109 to +130

dependency-audit:
runs-on: ubuntu-latest
steps:
Expand Down
50 changes: 50 additions & 0 deletions .github/workflows/performance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Performance contracts

on:
pull_request:
branches: [main]
workflow_dispatch:
schedule:
- cron: "41 5 * * 2"

permissions:
contents: read

env:
CARGO_TERM_COLOR: always

jobs:
deterministic-contracts:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Set up stable Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c
with:
toolchain: stable

- name: Verify allocation contracts
run: cargo test --test allocation_contract --all-features -- --test-threads=1

- name: Verify layout contracts
run: cargo test --test layout_snapshot --all-features -- --nocapture

- name: Capture shared-backing evidence
shell: bash
run: |
cargo bench --bench shared_backing -- __allocation_evidence_only__ --noplot 2>&1 |
tee target/allocation-evidence.log

- name: Verify shared-backing evidence
run: python scripts/verify-allocation-evidence.py target/allocation-evidence.log

- name: Upload allocation evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: allocation-evidence
path: target/allocation-evidence.log
if-no-files-found: error
retention-days: 14
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ jobs:
- name: Test no-default feature matrix
run: cargo test --no-default-features --features serde,bytes,simd

- name: Verify allocation contracts
run: cargo test --test allocation_contract --all-features -- --test-threads=1

- name: Verify layout contracts
run: cargo test --test layout_snapshot --all-features -- --nocapture

- name: Check packaged MSRV consumers
run: bash scripts/check-msrv-package.sh 1.95

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
- Removed the unmaintained `smartstring` benchmark dependency and retired the
stale score-verification scripts whose evidence manifest was not shipped.

### Performance and API

- Added `CheetahString::from_arc_str` and `From<Arc<str>>`; long values preserve
the input payload pointer with zero allocation, while short values remain
canonical inline strings.
- Removed temporary heap allocation from `From<char>` and inline-sized
concatenation.
- Exposed the reverse, clone, and fused iterator guarantees of `lines()`.
- Added fail-closed allocation evidence for exact/spare freezes, shared input,
cloning, characters, and short concatenation.

### Migration

- Replace read-only `PackedCheetahString` values with `CheetahString`.
Expand Down
53 changes: 53 additions & 0 deletions PERFORMANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Performance contracts

CheetahString separates deterministic performance contracts from timing
measurements. Allocation counts and object layout are merge/release gates;
Criterion timing and RSS observations are diagnostic because hosted runner
noise cannot establish a portable latency threshold.

## Enforced 64-bit contracts

| Operation | Maximum allocation events | Additional invariant |
|---|---:|---|
| Inline construction | 0 | UTF-8 length is at most 23 bytes |
| Static construction | 0 | Explicit static input remains borrowed |
| Long `Arc<str>` adoption | 0 | Payload pointer is retained |
| `char` construction | 0 | Every Unicode scalar remains inline |
| Concatenation with a result of at most 23 bytes | 0 | Result remains inline |
| Long shared clone | 0 | Payload pointer is shared |
| Long borrowed construction | 1 | One live `Arc<str>` backing |
| Exact-capacity owned/builder freeze | 1 | One live `Arc<str>` backing |
| Spare-capacity owned/builder freeze | 2 | Shrink/reallocation plus `Arc<str>` backing |
| `CheetahString` object size | N/A | 32 bytes on supported 64-bit targets |

The allocation count includes allocation and reallocation events during the
measured conversion. It is intentionally different from the number of live
allocations retained by the result.

## Reproducing the gates

Run the direct contracts:

```bash
cargo test --test allocation_contract --all-features -- --test-threads=1
cargo test --test layout_snapshot --all-features -- --nocapture
```

Capture and validate the independent benchmark evidence:

```bash
cargo bench --bench shared_backing -- __allocation_evidence_only__ --noplot \
2>&1 | tee target/allocation-evidence.log
python scripts/verify-allocation-evidence.py target/allocation-evidence.log
```

The verifier requires one schema-v2 `SHARED_BACKING_EVIDENCE` record and fails
closed when a required field is absent, an allocation count regresses, the
64-bit layout changes, or long `Arc<str>` input does not retain its pointer.

## Timing policy

Criterion groups remain useful for comparing construction, cloning, inline
fast paths, and RocketMQ-shaped map workloads on the same machine. Their raw
timings and RSS samples must be reported with toolchain, CPU, operating system,
feature set, and source revision. They are not used as cross-run release gates.
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ architecture.

## Design contract

| Storage | Condition | Construction allocation | Clone allocation |
| Input path | Storage | Allocation events during conversion | Clone allocation |
|---|---|---:|---:|
| Inline | UTF-8 length ≤ 23 bytes | 0 | 0 |
| Static | `&'static str` | 0 | 0 |
| Shared | Other long text | 1 live backing allocation | 0 |
| Explicit `from_static_str` | Static | 0 | 0 |
| Other UTF-8 input ≤ 23 bytes | Inline | 0 | 0 |
| Long `Arc<str>` | Shared | 0; payload pointer is retained | 0 |
| Long borrowed text or exact-capacity `String` | Shared | 1 | 0 |
| Long spare-capacity `String` / builder | Shared | 2: shrink/reallocate, then Arc backing | 0 |

The representation has no mutable `Owned(String)` state. Construction history
therefore cannot change clone complexity. Use:
Expand Down Expand Up @@ -62,12 +64,16 @@ use cheetah_string::{CheetahBuilder, CheetahString};
let inline = CheetahString::from("orders");
let static_value = CheetahString::from_static_str("system-topic");
let shared = CheetahString::from_string("long-dynamic-value-".repeat(8));
let adopted = CheetahString::from(std::sync::Arc::<str>::from(
"ownership-preserving-shared-value",
));
let cloned = shared.clone();

assert_eq!(inline, "orders");
assert_eq!(static_value, "system-topic");
assert_eq!(shared, cloned);
assert_eq!(shared.as_bytes().as_ptr(), cloned.as_bytes().as_ptr());
assert_eq!(adopted, "ownership-preserving-shared-value");

let mut builder = CheetahBuilder::with_capacity(64);
builder.push_str("orders");
Expand Down Expand Up @@ -106,6 +112,9 @@ assert_eq!(forward, ["a", "b", "c"]);
let csv = CheetahString::from("a,b,c");
let reverse: Vec<_> = csv.split_char(',').rev().collect();
assert_eq!(reverse, ["c", "b", "a"]);

let reverse_lines: Vec<_> = CheetahString::from("a\nb\nc").lines().rev().collect();
assert_eq!(reverse_lines, ["c", "b", "a"]);
```

`split_str` is intentionally forward-only. Unsupported reverse iteration fails
Expand Down Expand Up @@ -170,6 +179,9 @@ dedicated fixed CPU with two reversed base/head rounds.
```bash
cargo test --test layout_snapshot --all-features
cargo test --test allocation_contract --all-features -- --test-threads=1
cargo bench --bench shared_backing -- __allocation_evidence_only__ --noplot \
2>&1 | tee target/allocation-evidence.log
python scripts/verify-allocation-evidence.py target/allocation-evidence.log
cargo bench --bench comprehensive
cargo bench --bench mq_properties
cargo bench --bench mq_remoting_header
Expand All @@ -179,6 +191,8 @@ cargo bench --bench mq_topic
Hosted-runner and local benchmark results are diagnostic; they do not
independently establish a release-grade performance pass. The versioned
allocation and layout tests are the deterministic performance contracts.
See [Performance contracts](PERFORMANCE.md) for the exact enforced budgets and
the distinction between deterministic gates and diagnostic timing results.

## Safety and portability

Expand Down
61 changes: 60 additions & 1 deletion benches/shared_backing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,26 @@ fn emit_allocation_evidence() {
let (arc_string_clone_allocs, arc_string_clone_bytes, arc_string_clone) =
allocation_delta(|| arc_string_exact.clone());

let (cheetah_borrowed_allocs, cheetah_borrowed_bytes, cheetah_borrowed) =
allocation_delta(|| CheetahString::from_slice(&long));
let cheetah_exact_input = exact_string(1024);
let (cheetah_exact_allocs, cheetah_exact_bytes, cheetah_exact) =
allocation_delta(|| CheetahString::from_string(cheetah_exact_input));
let cheetah_spare_input = spare_string(1024);
let (cheetah_spare_allocs, cheetah_spare_bytes, cheetah_spare) =
allocation_delta(|| CheetahString::from_string(cheetah_spare_input));
let shared_source: Arc<str> = Arc::from(long.as_str());
let shared_source_pointer = shared_source.as_ptr();
let (cheetah_arc_allocs, cheetah_arc_bytes, cheetah_arc) =
allocation_delta(|| CheetahString::from_arc_str(shared_source));
let (cheetah_clone_allocs, cheetah_clone_bytes, cheetah_clone) =
allocation_delta(|| cheetah_exact.clone());
let (cheetah_char_allocs, cheetah_char_bytes, cheetah_char) =
allocation_delta(|| CheetahString::from('🦀'));
let short_left = CheetahString::from("inline");
let (cheetah_concat_allocs, cheetah_concat_bytes, cheetah_concat) =
allocation_delta(|| short_left + "-value");

let arc_str_rss_before = current_rss_bytes();
let retained_arc_str = (0..10_000)
.map(|index| ArcStrCandidate::owned(format!("RMQ_SYS_TRACE_TOPIC_{index:05}")))
Expand All @@ -259,7 +279,7 @@ fn emit_allocation_evidence() {
let arc_string_rss_after = current_rss_bytes();

let evidence = json!({
"schema_version": 1,
"schema_version": 2,
"object_sizes": {
"Inline|Arc<str>": size_of::<ArcStrCandidate>(),
"Inline|Arc<String>": size_of::<ArcStringCandidate>(),
Expand All @@ -277,8 +297,20 @@ fn emit_allocation_evidence() {
"owned_exact": {"count": arc_string_exact_allocs, "bytes": arc_string_exact_bytes},
"owned_spare": {"count": arc_string_spare_allocs, "bytes": arc_string_spare_bytes},
"clone": {"count": arc_string_clone_allocs, "bytes": arc_string_clone_bytes}
},
"CheetahString": {
"borrowed": {"count": cheetah_borrowed_allocs, "bytes": cheetah_borrowed_bytes},
"owned_exact": {"count": cheetah_exact_allocs, "bytes": cheetah_exact_bytes},
"owned_spare": {"count": cheetah_spare_allocs, "bytes": cheetah_spare_bytes},
"from_arc_str": {"count": cheetah_arc_allocs, "bytes": cheetah_arc_bytes},
"clone": {"count": cheetah_clone_allocs, "bytes": cheetah_clone_bytes},
"char": {"count": cheetah_char_allocs, "bytes": cheetah_char_bytes},
"short_concat": {"count": cheetah_concat_allocs, "bytes": cheetah_concat_bytes}
}
},
"invariants": {
"from_arc_str_pointer_reused": cheetah_arc.as_bytes().as_ptr() == shared_source_pointer
},
"rss": {
"Arc<str>": {
"before_bytes": arc_str_rss_before,
Expand All @@ -305,6 +337,13 @@ fn emit_allocation_evidence() {
arc_string_exact,
arc_string_spare,
arc_string_clone,
cheetah_borrowed,
cheetah_exact,
cheetah_spare,
cheetah_arc,
cheetah_clone,
cheetah_char,
cheetah_concat,
retained_arc_string,
));
}
Expand All @@ -323,6 +362,10 @@ fn bench_construction(c: &mut Criterion) {
group.bench_function("CheetahString/shared/borrowed", |b| {
b.iter(|| black_box(CheetahString::from(black_box(borrowed.as_str()))))
});
let shared: Arc<str> = Arc::from(borrowed.as_str());
group.bench_function("CheetahString/shared/from_arc_str", |b| {
b.iter(|| CheetahString::from_arc_str(black_box(Arc::clone(&shared))))
});
group.bench_function("Arc<str>/owned_exact", |b| {
b.iter_batched(
|| exact_string(1024),
Expand Down Expand Up @@ -401,6 +444,21 @@ fn bench_construction(c: &mut Criterion) {
group.finish();
}

fn bench_inline_fast_paths(c: &mut Criterion) {
let mut group = c.benchmark_group("inline_fast_paths");
group.bench_function("char", |b| {
b.iter(|| black_box(CheetahString::from(black_box('🦀'))))
});
group.bench_function("short_concat", |b| {
b.iter_batched(
|| CheetahString::from("inline"),
|left| black_box(left + black_box("-value")),
BatchSize::SmallInput,
)
});
group.finish();
}

fn bench_clone(c: &mut Criterion) {
let value = exact_string(1024);
let arc_str = ArcStrCandidate::owned(value.clone());
Expand Down Expand Up @@ -566,6 +624,7 @@ fn benchmarks(c: &mut Criterion) {
emit_allocation_evidence();
bench_construction(c);
bench_clone(c);
bench_inline_fast_paths(c);
bench_mq_workloads(c);
}

Expand Down
15 changes: 12 additions & 3 deletions scripts/bench-all.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ $benchmarkIds = @(

[ordered]@{
schema_version = 1
capture_schema_version = "cheetah-string-capture-v2"
benchmark_schema_version = "cheetah-string-bench-v1"
capture_schema_version = "cheetah-string-capture-v3"
benchmark_schema_version = "cheetah-string-bench-v2"
criterion_schema_version = "criterion-0.5"
crate = "cheetah-string"
git_sha = $gitSha
Expand Down Expand Up @@ -154,10 +154,15 @@ Invoke-CargoCapture "allocation-contract.txt" @(
)
Assert-TestExecuted "allocation-contract.txt"
[ordered]@{
schema_version = 1
schema_version = 2
layout_contract = "passed"
allocation_contract = "passed"
clone_allocations_max = 0
arc_str_adoption_allocations_max = 0
char_allocations_max = 0
inline_concat_allocations_max = 0
owned_exact_freeze_allocations_max = 1
owned_spare_freeze_allocations_max = 2
source = "tests/allocation_contract.rs"
} | ConvertTo-Json -Depth 4 | Set-Content -Encoding utf8 -LiteralPath (Join-Path $ResultDir "contracts.json")
Invoke-CargoCapture "layout-bench.txt" (@(
Expand Down Expand Up @@ -187,6 +192,10 @@ Invoke-CargoCapture "simd.txt" (@(
Invoke-CargoCapture "shared-backing.txt" (@(
"bench", "--bench", "shared_backing", "--"
) + $criterionArguments)
& python (Join-Path $PSScriptRoot "verify-allocation-evidence.py") (Join-Path $ResultDir "shared-backing.txt")
if ($LASTEXITCODE -ne 0) {
throw "shared-backing allocation evidence verification failed"
}

if (-not (Test-Path -LiteralPath $CriterionSource)) {
throw "Criterion result directory is missing: $CriterionSource"
Expand Down
Loading
Loading