The Rust worker is the product: edge/, built and checked in CI
User: Rust is the product (option a). tools/edge-bench-rs moves to edge/
(crate edge; binaries edge-node, the worker, and edge-bench, the replay).
CI's new job edge runs cargo test and edge/check-fixtures.sh: both
RangeIndexers on three committed captures (edge/fixtures, 25k overview
lines each of a.b.models, a.b.ath and a.b.boneless in 5 chunks, 3.5 MB),
compared field by field with check.ts; deploy waits for it. The
TypeScript RangeIndexer stays as check.ts's reference. README: what it
is now; the benchmark findings as history.
Random names: looksRandom missed long tokens of capitals and digits
("51294P1CUKR2R35X", release 74539068, readable and a Newznab title).
12+ capitals and digits switching 6+ times between them are random
(COD4MW2REMASTERED switches 4 times, BEATLES1964REMASTER twice); both
implementations, identical on the fixtures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
48f4e982f9
commit
5109481d67
38 files changed
+138
-54
No files matched your search
+1
-1
@@ -3,4 +3,4 @@ build
|
|||||||
tmp
|
tmp
|
||||||
.env
|
.env
|
||||||
.git
|
.git
|
||||||
tools/edge-bench-rs/target
|
edge/target
|
||||||
@@ -66,8 +66,28 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: node ace test
|
run: node ace test
|
||||||
|
|
||||||
|
# The edge worker (edge/, Rust): its tests, and the Rust and TypeScript
|
||||||
|
# RangeIndexers compared on the committed captures (edge/fixtures).
|
||||||
|
edge:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- name: Rust toolchain
|
||||||
|
run: |
|
||||||
|
command -v cargo || curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
|
||||||
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: cargo test
|
||||||
|
run: cargo test --release --manifest-path edge/Cargo.toml
|
||||||
|
- name: Rust and TypeScript identical on the fixtures
|
||||||
|
run: edge/check-fixtures.sh
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: test
|
needs: [test, edge]
|
||||||
if: github.ref == 'refs/heads/rebuild'
|
if: github.ref == 'refs/heads/rebuild'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# Voyager 2's images (deploy/compose.yaml):
|
# Voyager 2's images (deploy/compose.yaml):
|
||||||
# --target app the AdonisJS app: web server, coordinator, migrations, ace commands
|
# --target app the AdonisJS app: web server, coordinator, migrations, ace commands
|
||||||
# --target edge the Rust edge worker (tools/edge-bench-rs, bin edge-node)
|
# --target edge the Rust edge worker (edge/, bin edge-node)
|
||||||
|
|
||||||
# Node is pinned as in CI (.nvmrc): on 24.21, jsonschema 1.5.0 (used by Ace to
|
# Node is pinned as in CI (.nvmrc): on 24.21, jsonschema 1.5.0 (used by Ace to
|
||||||
# validate commands) throws "Invalid URL" on $ref, so no ace command starts.
|
# validate commands) throws "Invalid URL" on $ref, so no ace command starts.
|
||||||
@@ -29,7 +29,7 @@ CMD ["node", "ace", "list"]
|
|||||||
# The Rust worker. Registry and target dir are build caches, so a rebuild only compiles what changed.
|
# The Rust worker. Registry and target dir are build caches, so a rebuild only compiles what changed.
|
||||||
FROM rust:1-bookworm AS edge-build
|
FROM rust:1-bookworm AS edge-build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY tools/edge-bench-rs ./
|
COPY edge ./
|
||||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
--mount=type=cache,target=/src/target \
|
--mount=type=cache,target=/src/target \
|
||||||
cargo build --release --locked --bin edge-node \
|
cargo build --release --locked --bin edge-node \
|
||||||
|
|||||||
+2
-2
@@ -119,7 +119,7 @@ dropping garbage; 28 TB of compressed overview traffic. Throughput goal:
|
|||||||
2. **Edge worker** (node, TypeScript reusing `lib/`). Measured (`edge:bench`,
|
2. **Edge worker** (node, TypeScript reusing `lib/`). Measured (`edge:bench`,
|
||||||
2026-10-09, after two rounds of work): ~200–215 k headers/s per core on
|
2026-10-09, after two rounds of work): ~200–215 k headers/s per core on
|
||||||
recent a.b.boneless, ~320–340 k on a 2013 a.b.ath range, ~0.7 GB peak
|
recent a.b.boneless, ~320–340 k on a 2013 a.b.ath range, ~0.7 GB peak
|
||||||
per process. **Rust** (`tools/edge-bench-rs`, the same RangeIndexer path
|
per process. **Rust** (`edge/`, the same RangeIndexer path
|
||||||
over the same captures; `check.ts` finds every output identical, the
|
over the same captures; `check.ts` finds every output identical, the
|
||||||
decoded payloads included): 3.5–3.8× less CPU (boneless 2.9 vs 11.1
|
decoded payloads included): 3.5–3.8× less CPU (boneless 2.9 vs 11.1
|
||||||
CPU-s per 2 M headers, ath 2.15 vs 7.5) and a third of the memory
|
CPU-s per 2 M headers, ath 2.15 vs 7.5) and a third of the memory
|
||||||
@@ -130,7 +130,7 @@ dropping garbage; 28 TB of compressed overview traffic. Throughput goal:
|
|||||||
cost: a second implementation of lib/headers, lib/release, lib/rules and
|
cost: a second implementation of lib/headers, lib/release, lib/rules and
|
||||||
the chunk format, kept identical (check.ts) for as long as both exist.
|
the chunk format, kept identical (check.ts) for as long as both exist.
|
||||||
Findings, how to run both and how to compare their outputs:
|
Findings, how to run both and how to compare their outputs:
|
||||||
`tools/edge-bench-rs/README.md`. Since 2026-10-09 the Rust worker
|
`edge/README.md`. Since 2026-10-09 the Rust worker
|
||||||
(`edge-node`, same crate) is the one running on homelab: ~8× less CPU
|
(`edge-node`, same crate) is the one running on homelab: ~8× less CPU
|
||||||
per header and 20× less memory than `edge:node` in production.
|
per header and 20× less memory than `edge:node` in production.
|
||||||
Per lease: compressed chunks of 10–50 k lines, in order →
|
Per lease: compressed chunks of 10–50 k lines, in order →
|
||||||
|
|||||||
File renamed without changes.
+1
-1
@@ -141,7 +141,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "edge-bench-rs"
|
name = "edge"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "edge-bench-rs"
|
name = "edge"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "The edge worker's indexing (lib/edge RangeIndexer) in Rust, to compare with edge:bench on captured ranges"
|
description = "Voyager 2's edge worker (edge-node) and its benchmark (edge-bench): the RangeIndexer in Rust, kept identical to lib/edge (check.ts)"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "edge-bench"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "edge-node"
|
||||||
|
path = "src/bin/edge-node.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
@@ -1,36 +1,40 @@
|
|||||||
# edge-bench-rs: the edge worker's indexing in Rust
|
# edge: voyager 2's edge worker (Rust)
|
||||||
|
|
||||||
Since 2026-10-09 this crate is also the production worker: `edge-node`
|
The product: `edge-node` (`src/bin/edge-node.rs`) is the worker that runs
|
||||||
(`src/bin/edge-node.rs`) leases ranges from the coordinator and posts
|
on homelab (compose service `edge`, image target `edge`). It leases ranges
|
||||||
batches like `node ace edge:node`, with the same indexing as the
|
from the node API, reads their prefetched XOVER chunks, finds releases and
|
||||||
benchmark (`src/lib.rs`). On homelab it indexed 122M headers in 10
|
posts batches. `edge-bench` (`src/main.rs`) replays a captured range through
|
||||||
minutes with 0.37 cores and 565 MB, next to the TypeScript workers' 24M
|
the same indexing (`src/lib.rs`) for timing and for comparing.
|
||||||
with 0.61 cores and 11.3 GB (8 threads vs 8 processes): about 8× less
|
|
||||||
CPU per header and 20× less memory. See "The worker" below.
|
|
||||||
|
|
||||||
A measurement, not a product. It answers whether a Rust edge worker would be
|
The TypeScript `RangeIndexer` (`lib/edge`, `node ace edge:node`) is the
|
||||||
worth a second implementation of the TypeScript one (`edge:node`, REBUILD.md
|
reference: `check.ts` runs both on a capture and compares every output field
|
||||||
part 2). It ports the path that `edge:bench` replays over a captured range:
|
by field. An identity or rule change goes into both and has to come out
|
||||||
|
identical. CI (`.gitea/workflows/deploy.yaml`, job `edge`) runs `cargo test`
|
||||||
|
and `check-fixtures.sh`, which compares both on the committed captures in
|
||||||
|
`fixtures/` (25k overview lines each of a.b.models, a.b.ath and
|
||||||
|
a.b.boneless, in 5 chunks); a push to `rebuild` deploys only when they
|
||||||
|
match.
|
||||||
|
|
||||||
- inflate each XOVER chunk;
|
```sh
|
||||||
- scan the overview lines (no object per line);
|
cargo test --release --manifest-path edge/Cargo.toml
|
||||||
- apply the owner rule from the Xref;
|
edge/check-fixtures.sh # from the repository root
|
||||||
- build segments (file key, release identity);
|
```
|
||||||
- aggregate segments into files, and cut off idle ones as fragments,
|
|
||||||
per-article or incomplete files;
|
|
||||||
- every N chunks, judge the completed releases (rules: executables,
|
|
||||||
readable names, garbage tools and signatures, per-article random
|
|
||||||
subjects), encode their chunks (format v2) and pick probe candidates.
|
|
||||||
|
|
||||||
It does not port NNTP, the coordinator API, batch posting or anything on the
|
In production since 2026-10-09: 122M headers in 10 minutes with 0.37 cores
|
||||||
server side.
|
and 565 MB, next to the TypeScript workers' 24M with 0.61 cores and 11.3 GB
|
||||||
|
(8 threads vs 8 processes): about 8× less CPU per header and 20× less
|
||||||
|
memory. See "The worker" below.
|
||||||
|
|
||||||
|
It started as a measurement (moved from `tools/edge-bench-rs` on
|
||||||
|
2026-10-11): whether a Rust worker would be worth a second implementation.
|
||||||
|
The findings below are from then.
|
||||||
|
|
||||||
## Findings (2026-10-09)
|
## Findings (2026-10-09)
|
||||||
|
|
||||||
Same machine (the workstation, otherwise idle), same captures, 2M headers
|
Same machine (the workstation, otherwise idle), same captures, 2M headers
|
||||||
each:
|
each:
|
||||||
|
|
||||||
| Capture | TypeScript (`edge:bench`) | Rust (`edge-bench-rs`) | Less CPU |
|
| Capture | TypeScript (`edge:bench`) | Rust (`edge-bench`) | Less CPU |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| a.b.boneless 2026 (160950000000–160951999999) | 11.1 CPU-s (~215k headers/s), 0.70 GB peak | 2.9 CPU-s (~690k/s), 0.24 GB | 3.8× |
|
| a.b.boneless 2026 (160950000000–160951999999) | 11.1 CPU-s (~215k headers/s), 0.70 GB peak | 2.9 CPU-s (~690k/s), 0.24 GB | 3.8× |
|
||||||
| a.b.boneless 2026 (160952000000–160953999999) | 11.8 CPU-s (~205k/s), 0.70 GB | 3.1 CPU-s (~640k/s), 0.24 GB | 3.8× |
|
| a.b.boneless 2026 (160952000000–160953999999) | 11.8 CPU-s (~205k/s), 0.70 GB | 3.1 CPU-s (~640k/s), 0.24 GB | 3.8× |
|
||||||
@@ -92,8 +96,8 @@ Benchmark both on it:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
node ace edge:bench --dir /path/to/capture # TypeScript: headers/s, CPU, inflate/index, peak RSS
|
node ace edge:bench --dir /path/to/capture # TypeScript: headers/s, CPU, inflate/index, peak RSS
|
||||||
cd tools/edge-bench-rs && cargo build --release
|
cd edge && cargo build --release
|
||||||
./target/release/edge-bench-rs /path/to/capture # Rust: the same summary
|
./target/release/edge-bench /path/to/capture # Rust: the same summary
|
||||||
```
|
```
|
||||||
|
|
||||||
Both take results every 10 chunks (`--batch-chunks`) and give a file up after
|
Both take results every 10 chunks (`--batch-chunks`) and give a file up after
|
||||||
@@ -109,8 +113,8 @@ themselves.
|
|||||||
## Comparing the output
|
## Comparing the output
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./target/release/edge-bench-rs /path/to/capture --dump /tmp/out.jsonl
|
./target/release/edge-bench /path/to/capture --dump /tmp/out.jsonl
|
||||||
cd ../.. && node --import=@poppinss/ts-exec tools/edge-bench-rs/check.ts /path/to/capture /tmp/out.jsonl
|
cd .. && node --import=@poppinss/ts-exec edge/check.ts /path/to/capture /tmp/out.jsonl
|
||||||
```
|
```
|
||||||
|
|
||||||
`--dump` writes every output of every batch as JSON lines:
|
`--dump` writes every output of every batch as JSON lines:
|
||||||
@@ -223,12 +227,12 @@ COORDINATOR_URL=http://coordinator:3334 NODE_TOKEN=… ./target/release/edge-nod
|
|||||||
node's download cap, shared by its threads. Plus a heartbeat when no
|
node's download cap, shared by its threads. Plus a heartbeat when no
|
||||||
batch went out for a minute.
|
batch went out for a minute.
|
||||||
- **Wire format:** `src/wire.rs` writes a batch as `toWire` does.
|
- **Wire format:** `src/wire.rs` writes a batch as `toWire` does.
|
||||||
`edge-bench-rs --wire` and `check.ts --wire` compare it with the
|
`edge-bench --wire` and `check.ts --wire` compare it with the
|
||||||
TypeScript's, field by field.
|
TypeScript's, field by field.
|
||||||
- **End to end:** both workers indexed the a.b.models capture (spooled
|
- **End to end:** both workers indexed the a.b.models capture (spooled
|
||||||
as a range) through the web server; what the server stored was the
|
as a range) through the web server; what the server stored was the
|
||||||
same (releases with every column, chunks, candidates, rule counts,
|
same (releases with every column, chunks, candidates, rule counts,
|
||||||
fragments with their JSON).
|
fragments with their JSON).
|
||||||
- **On homelab:** container `usenet-rebuild-edge-rs` runs
|
- **On homelab:** compose service `edge` (deploy/compose.yaml), the
|
||||||
`/mnt/bulk/usenet-rebuild/bin/edge-node --threads 8` in
|
`edge` image target of the Dockerfile, `--threads $EDGE_THREADS` (8);
|
||||||
node:22-bookworm-slim (the binary needs glibc 2.34+).
|
it talks to `node-api`.
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# The Rust and TypeScript RangeIndexers on the committed captures (edge/fixtures: 25k
|
||||||
|
# overview lines each of a.b.models, a.b.ath and a.b.boneless, in 5 chunks), compared
|
||||||
|
# by check.ts field by field: exits non-zero on any difference. CI runs it; run it from
|
||||||
|
# the repository root after any change to either side.
|
||||||
|
set -eu
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
cargo build --release --quiet --manifest-path edge/Cargo.toml --bin edge-bench
|
||||||
|
out=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$out"' EXIT
|
||||||
|
status=0
|
||||||
|
for capture in edge/fixtures/*/; do
|
||||||
|
name=$(basename "$capture")
|
||||||
|
edge/target/release/edge-bench "$capture" --batch-chunks 2 --idle 2 --dump "$out/$name.jsonl" --wire "$out/$name.wire.jsonl" >/dev/null 2>&1
|
||||||
|
echo "== $name"
|
||||||
|
node --import=@poppinss/ts-exec edge/check.ts "$capture" "$out/$name.jsonl" --wire "$out/$name.wire.jsonl" --batch-chunks 2 --idle 2 || status=1
|
||||||
|
done
|
||||||
|
exit $status
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Compares edge-bench-rs's output (--dump) with the TypeScript RangeIndexer
|
* Compares edge-bench's output (--dump) with the TypeScript RangeIndexer
|
||||||
* on the same captured range, item by item: releases (every field, and the
|
* on the same captured range, item by item: releases (every field, and the
|
||||||
* payload decoded with lib/chunks), samples, probe candidates, rule counts,
|
* payload decoded with lib/chunks), samples, probe candidates, rule counts,
|
||||||
* fragments and batch boundaries.
|
* fragments and batch boundaries.
|
||||||
*
|
*
|
||||||
* node --import=@poppinss/ts-exec tools/edge-bench-rs/check.ts <dir> <dump.jsonl> [--wire batches.jsonl] [--batch-chunks 10] [--idle 5]
|
* node --import=@poppinss/ts-exec edge/check.ts <dir> <dump.jsonl> [--wire batches.jsonl] [--batch-chunks 10] [--idle 5]
|
||||||
*
|
*
|
||||||
* With --wire (edge-bench-rs --wire): also each batch as the Rust worker
|
* With --wire (edge-bench --wire): also each batch as the Rust worker
|
||||||
* posts it, against toWire of the TypeScript batch, field by field.
|
* posts it, against toWire of the TypeScript batch, field by field.
|
||||||
*/
|
*/
|
||||||
import { readFile, readdir } from 'node:fs/promises'
|
import { readFile, readdir } from 'node:fs/promises'
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"group":"alt.binaries.ath","start":2549992990,"end":2550017989}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"group":"alt.binaries.boneless","start":160950000000,"end":160950024999}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
{"group":"alt.binaries.models","start":2017215,"end":2042214}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! The edge worker in Rust (REBUILD.md, part 2; commands/edge_node.ts): leases
|
//! The edge worker in Rust (REBUILD.md, part 2; commands/edge_node.ts): leases
|
||||||
//! ranges from the coordinator, reads their XOVER chunks from its stream,
|
//! ranges from the coordinator, reads their XOVER chunks from its stream,
|
||||||
//! indexes them (the same RangeIndexer as edge-bench-rs, kept identical to
|
//! indexes them (the same RangeIndexer as edge-bench, kept identical to
|
||||||
//! the TypeScript by check.ts) and posts the batches. One range per thread.
|
//! the TypeScript by check.ts) and posts the batches. One range per thread.
|
||||||
//!
|
//!
|
||||||
//! COORDINATOR_URL=… NODE_TOKEN=… edge-node [--threads N] [--batch-articles 500000] [--ranges N] [--max-mbit M]
|
//! COORDINATOR_URL=… NODE_TOKEN=… edge-node [--threads N] [--batch-articles 500000] [--ranges N] [--max-mbit M]
|
||||||
@@ -8,9 +8,9 @@
|
|||||||
//! SIGTERM/SIGINT: each thread gives its range back after the current chunk
|
//! SIGTERM/SIGINT: each thread gives its range back after the current chunk
|
||||||
//! (the next worker resumes at the last stored batch).
|
//! (the next worker resumes at the last stored batch).
|
||||||
|
|
||||||
use edge_bench_rs::indexer::{Batch, RangeIndexer};
|
use edge::indexer::{Batch, RangeIndexer};
|
||||||
use edge_bench_rs::names::Rules;
|
use edge::names::Rules;
|
||||||
use edge_bench_rs::wire;
|
use edge::wire;
|
||||||
use flate2::write::GzEncoder;
|
use flate2::write::GzEncoder;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,5 +1,5 @@
|
|||||||
//! The edge worker's indexing (lib/edge RangeIndexer and what it uses) in
|
//! The edge worker's indexing (lib/edge RangeIndexer and what it uses) in
|
||||||
//! Rust, kept identical to the TypeScript (check.ts): the edge-bench-rs
|
//! Rust, kept identical to the TypeScript (check.ts): the edge-bench
|
||||||
//! benchmark and the edge-node worker share it.
|
//! benchmark and the edge-node worker share it.
|
||||||
|
|
||||||
pub mod codec;
|
pub mod codec;
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
//! --capture` (REBUILD.md, part 2: TypeScript or Rust). Same work as
|
//! --capture` (REBUILD.md, part 2: TypeScript or Rust). Same work as
|
||||||
//! edge:bench's replay: inflate each chunk, scan it, aggregate files, cut off
|
//! edge:bench's replay: inflate each chunk, scan it, aggregate files, cut off
|
||||||
//! idle ones, and every N chunks judge and encode the releases. `--dump`
|
//! idle ones, and every N chunks judge and encode the releases. `--dump`
|
||||||
//! writes every output as JSON lines for tools/edge-bench-rs/check.ts.
|
//! writes every output as JSON lines for edge/check.ts.
|
||||||
//!
|
//!
|
||||||
//! edge-bench-rs <dir> [--batch-chunks 10] [--idle 5] [--dump out.jsonl] [--wire batches.jsonl]
|
//! edge-bench <dir> [--batch-chunks 10] [--idle 5] [--dump out.jsonl] [--wire batches.jsonl]
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use edge_bench_rs::indexer::{self, Batch, RangeIndexer};
|
use edge::indexer::{self, Batch, RangeIndexer};
|
||||||
use edge_bench_rs::js::json_str;
|
use edge::js::json_str;
|
||||||
use edge_bench_rs::{names, wire};
|
use edge::{names, wire};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -150,7 +150,7 @@ fn dump_batch(batch: &Batch, out: &mut impl Write) {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let dir = args.get(1).expect("usage: edge-bench-rs <dir> [--batch-chunks N] [--idle N] [--dump file]").clone();
|
let dir = args.get(1).expect("usage: edge-bench <dir> [--batch-chunks N] [--idle N] [--dump file]").clone();
|
||||||
let flag = |name: &str| args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).cloned();
|
let flag = |name: &str| args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).cloned();
|
||||||
let batch_chunks: usize = flag("--batch-chunks").map(|v| v.parse().unwrap()).unwrap_or(10);
|
let batch_chunks: usize = flag("--batch-chunks").map(|v| v.parse().unwrap()).unwrap_or(10);
|
||||||
let idle: u64 = flag("--idle").map(|v| v.parse().unwrap()).unwrap_or(5);
|
let idle: u64 = flag("--idle").map(|v| v.parse().unwrap()).unwrap_or(5);
|
||||||
@@ -591,6 +591,11 @@ pub fn looks_random(name: &str) -> bool {
|
|||||||
if (8..=15).contains(&b.len()) && alnum && n_digit > 0 && n_lower > 0 && b[1..].iter().filter(|c| c.is_ascii_uppercase()).count() >= 3 {
|
if (8..=15).contains(&b.len()) && alnum && n_digit > 0 && n_lower > 0 && b[1..].iter().filter(|c| c.is_ascii_uppercase()).count() >= 3 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// 51294P1CUKR2R35X: capitals and digits only, switching between them all over the token
|
||||||
|
// (COD4MW2REMASTERED switches 4 times, BEATLES1964REMASTER twice).
|
||||||
|
if b.len() >= 12 && b.iter().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()) && b.windows(2).filter(|w| w[0].is_ascii_digit() != w[1].is_ascii_digit()).count() >= 6 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if !(b.len() >= 16 && alnum) || n_lower == 0 || n_upper == 0 {
|
if !(b.len() >= 16 && alnum) || n_lower == 0 || n_upper == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -985,6 +990,9 @@ mod tests {
|
|||||||
fn random_names() {
|
fn random_names() {
|
||||||
assert!(looks_random("prvjr7hapd0i1ny14pz4"));
|
assert!(looks_random("prvjr7hapd0i1ny14pz4"));
|
||||||
assert!(looks_random("RmISakytFwXSCEzjVEpaKk"));
|
assert!(looks_random("RmISakytFwXSCEzjVEpaKk"));
|
||||||
|
assert!(looks_random("51294P1CUKR2R35X"));
|
||||||
|
assert!(!looks_random("COD4MW2REMASTERED"));
|
||||||
|
assert!(!looks_random("BEATLES1964REMASTER"));
|
||||||
assert!(!looks_random("Some.Show.S01E01.1080p"));
|
assert!(!looks_random("Some.Show.S01E01.1080p"));
|
||||||
assert!(looks_random("VIDEO_TS"));
|
assert!(looks_random("VIDEO_TS"));
|
||||||
}
|
}
|
||||||
File renamed without changes.
@@ -5,6 +5,17 @@ import { isGenericName } from './identity.js'
|
|||||||
* separators, and the other generated shapes seen on Usenet (LOG.md
|
* separators, and the other generated shapes seen on Usenet (LOG.md
|
||||||
* 2026-10-01 (3)). Kept in step with SQL's is_readable_name().
|
* 2026-10-01 (3)). Kept in step with SQL's is_readable_name().
|
||||||
*/
|
*/
|
||||||
|
/** How often a token goes from a letter to a digit or back. */
|
||||||
|
function letterDigitSwitches(token: string): number {
|
||||||
|
let switches = 0
|
||||||
|
for (let i = 1; i < token.length; i++) {
|
||||||
|
const a = token.charCodeAt(i - 1) <= 57
|
||||||
|
const b = token.charCodeAt(i) <= 57
|
||||||
|
if (a !== b) switches++
|
||||||
|
}
|
||||||
|
return switches
|
||||||
|
}
|
||||||
|
|
||||||
export function looksRandom(name: string): boolean {
|
export function looksRandom(name: string): boolean {
|
||||||
// An unknown short extension (e.g. ".bin") says nothing about the name.
|
// An unknown short extension (e.g. ".bin") says nothing about the name.
|
||||||
name = name.replace(/\.[A-Za-z0-9]{1,4}$/, '')
|
name = name.replace(/\.[A-Za-z0-9]{1,4}$/, '')
|
||||||
@@ -21,6 +32,9 @@ export function looksRandom(name: string): boolean {
|
|||||||
if (/^[a-z0-9]{12,}$/.test(name) && (name.match(/\d/g)?.length ?? 0) >= 2 && (name.match(/[a-z]/g)?.length ?? 0) >= 4) return true
|
if (/^[a-z0-9]{12,}$/.test(name) && (name.match(/\d/g)?.length ?? 0) >= 2 && (name.match(/[a-z]/g)?.length ?? 0) >= 4) return true
|
||||||
// wHJ7s71sAGE: short, but capitals all over the token, not only at word starts
|
// wHJ7s71sAGE: short, but capitals all over the token, not only at word starts
|
||||||
if (/^[A-Za-z0-9]{8,15}$/.test(name) && /\d/.test(name) && /[a-z]/.test(name) && (name.slice(1).match(/[A-Z]/g)?.length ?? 0) >= 3) return true
|
if (/^[A-Za-z0-9]{8,15}$/.test(name) && /\d/.test(name) && /[a-z]/.test(name) && (name.slice(1).match(/[A-Z]/g)?.length ?? 0) >= 3) return true
|
||||||
|
// 51294P1CUKR2R35X: capitals and digits only, switching between them all over the token
|
||||||
|
// (COD4MW2REMASTERED switches 4 times, BEATLES1964REMASTER twice).
|
||||||
|
if (/^[A-Z0-9]{12,}$/.test(name) && letterDigitSwitches(name) >= 6) return true
|
||||||
if (!/^[A-Za-z0-9]{16,}$/.test(name) || !/[a-z]/.test(name) || !/[A-Z]/.test(name)) return false
|
if (!/^[A-Za-z0-9]{16,}$/.test(name) || !/[a-z]/.test(name) || !/[A-Z]/.test(name)) return false
|
||||||
// Digits, or many capitals without separators (e.g. RmISakytFwXSCEzjVEpaKk).
|
// Digits, or many capitals without separators (e.g. RmISakytFwXSCEzjVEpaKk).
|
||||||
return /\d/.test(name) || (name.match(/[A-Z]/g) ?? []).length >= 6
|
return /\d/.test(name) || (name.match(/[A-Z]/g) ?? []).length >= 6
|
||||||
|
|||||||
@@ -78,6 +78,15 @@ test.group('release / releaseIdentity', () => {
|
|||||||
assert.equal(a.key, b.key)
|
assert.equal(a.key, b.key)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('capitals and digits switching all over a token are random', async ({ assert }) => {
|
||||||
|
const { looksRandom } = await import('#lib/release/index')
|
||||||
|
assert.isTrue(looksRandom('51294P1CUKR2R35X'))
|
||||||
|
assert.isTrue(looksRandom('51294P1CUKR2R35X.part01.rar'.replace(/\.part01\.rar$/, '')))
|
||||||
|
assert.isFalse(looksRandom('COD4MW2REMASTERED'))
|
||||||
|
assert.isFalse(looksRandom('BEATLES1964REMASTER'))
|
||||||
|
assert.isFalse(looksRandom('HDTV1080P'))
|
||||||
|
})
|
||||||
|
|
||||||
test('random extensionless names cannot be grouped by name', ({ assert }) => {
|
test('random extensionless names cannot be grouped by name', ({ assert }) => {
|
||||||
const name = '204b914a977e5f891590ff1099c7a8caf1a0be537bfa5bf7f3a8b1213231'
|
const name = '204b914a977e5f891590ff1099c7a8caf1a0be537bfa5bf7f3a8b1213231'
|
||||||
assert.isNull(releaseIdentity({ poster: 'x', subject: `"${name}" yEnc`, name, weak: true }))
|
assert.isNull(releaseIdentity({ poster: 'x', subject: `"${name}" yEnc`, name, weak: true }))
|
||||||
|
|||||||
Reference in new issue
Block a user