{"id":"synttx/scythe","name":"scythe","scope":"synttx","platform":"roblox","description":"A Data-Oriented Cleanup Library · Up to ×17 lighter than other maids","version":"1.2.0","latest":"1.2.0","versions":["1.1.1","1.1.2","1.2.0"],"license":"MPL-2.0","licenseRating":"caution","licenseCaveats":["File-level copyleft: if you modify this package's own source files, those modified files must be made available under MPL-2.0. Using it unmodified in a closed-source game is fine."],"licenseVerified":false,"dependencies":{},"integrity":"3b9488b3e17623048f0dca0b30ce8c3fadd673c1ace74be25f4f50fb909f891f","likes":0,"downloads":2,"install":"forest install synttx/scythe","url":"https://forest.dev/p/roblox/synttx/scythe","files":"https://api.forest.dev/ai/package/roblox/synttx/scythe/files","readme":"# Scythe 🔪🩸\n\n[![Version](https://img.shields.io/badge/version-1.2-E11D48?style=for-the-badge&logo=github&logoColor=white)](https://github.com/synttx/scythe/releases)\n[![License](https://img.shields.io/badge/license-MPL--2.0-3B82F6?style=for-the-badge&logo=mozilla&logoColor=white)](LICENSE)\n[![Luau](https://img.shields.io/badge/language-Luau-8B5CF6?style=for-the-badge&logo=luau&logoColor=white)](https://luau-lang.org)\n[![Dependencies](https://img.shields.io/badge/dependencies-zero-00A2FF?style=for-the-badge&logo=roblox&logoColor=white)](<>)\n\nA data-oriented cleanup library for Roblox Luau.\n\nUnlike conventional cleanup tools like [Janitor](https://github.com/howmanysmall/Janitor), [Maid](https://github.com/devSparkle/Maid), or [Trove](https://sleitnick.github.io/RbxUtil/api/Trove/), **Scythe** uses no objects, no metatables, no dependencies, and has zero per-instance allocation cost. A scope is simply an integer handle referencing module-level Structure-of-Arrays (SoA) storage.\n\n---\n\n## Installation\n\n### Via Wally\n\nAdd **Scythe** to your `wally.toml` dependencies:\n\n```toml\n[dependencies]\nScythe = \"synttx/scythe@1.2\"\n```\n\nThen run:\n\n```bash\nwally install\n```\n\n### Manual Installation\n\nDownload and copy the latest release module into your project:\n\n```luau\nlocal Scythe = require(path.to.Packages.Scythe)\n```\n\nThe module exports a `Scope` type alias for typed Luau:\n\n```luau\nlocal scope: Scythe.Scope = Scythe.scope()\n```\n\n---\n\n## Key Features\n\n- 🪶 **Up to 17× Lighter on Memory**: Uses integer handles and module-level buffers instead of heavy OOP tables, costing only **16 bytes** per scope (compared to up to 272 bytes in traditional libraries).\n- 🗑️ **Zero Garbage & GC Pressure**: Generates **0 bytes of heap garbage** in steady-state cleanup loops, completely eliminating GC frame stutters.\n- 🚀 **Blazing Fast Cleanup**: Pre-resolves disposal methods at `add` time so hot-loop cleanup runs without table lookups or method sniffing.\n- 🛡️ **Bug-Proof & Safe**: Automatically catches double-destroys with generation-tagged handles, and isolates errors so one failing callback never stops the rest from cleaning up.\n- ♻️ **Pooled & Resilient**: Scope handles are recycled automatically, and internal storage shrinks back down after memory spikes.\n- 🎯 **Strictly Typed & Zero Bloat**: Built with `--!strict` type safety, zero external dependencies, and no OOP boilerplate.\n\n---\n\n## Benchmarks\n\nScythe is architected from the ground up to eliminate Garbage Collector (GC) pressure and minimize CPU cache misses. By abandoning traditional Object-Oriented Programming (OOP) metatables and heap-allocated dictionaries in favor of module-level **Structure-of-Arrays (SoA)** buffers, Scythe achieves **zero-allocation steady-state cleanup** and an active memory footprint **10× to 17× smaller** than conventional cleanup libraries.\n\nAll benchmark scripts are located in [`benchmarks/`](benchmarks/) and were evaluated against [howmanysmall's Janitor](https://github.com/howmanysmall/Janitor), [Quenty's Maid](https://github.com/Quenty/NevermoreEngine/tree/main/src/maid), and [sleitnick's Trove](https://sleitnick.github.io/RbxUtil/api/Trove/) using [Scriptbench](https://github.com/bitsplicer/Scriptbench) and Roblox Studio.\n\n---\n\n### Memory Allocation & GC Pressure\n\nWhile execution speed benchmarks show Scythe performing at or above OOP leaders, **memory efficiency and GC pressure** are where Scythe completely demolishes all other libraries.\n\n```\n+-----------------------------------------------------------------------------------+\n|               STEADY-STATE GARBAGE GENERATION (1,000,000 CYCLES)                  |\n+-----------------------------------------------------------------------------------+\n|  Maid      █████████████████████████████████████  168.07 MB (172.11 B/cycle)      |\n|  Janitor   ███████████████████████████████████    161.00 MB (164.87 B/cycle)      |\n|  Trove     ██████████████████████████████████████████████████████... 1.42 GB      |\n|  Scythe    ▏                                        0.00 KB   (0.00 B/cycle)      |\n+-----------------------------------------------------------------------------------+\n```\n\n#### Peak Active RAM & GC Footprint Comparison\n\n| Library          | Cold Active RAM (100k Items) | Warm Active RAM (100k Items) | Bytes per Scope | Total GC Garbage (1M Cycles) | Bytes per Cycle |\n| :--------------- | :--------------------------- | :--------------------------- | :-------------- | :--------------------------- | :-------------- |\n| **Scythe (SoA)** | **1,562.00 KB**              | **1,562.00 KB**              | **15.99 B**     | **0.00 KB**                  | **0.00 B**      |\n| **Janitor**      | 12,500.00 KB                 | 12,500.00 KB                 | 128.00 B        | 161,002.00 KB _(~161 MB)_    | 164.87 B        |\n| **Maid**         | 15,625.00 KB                 | 15,625.00 KB                 | 160.00 B        | 168,073.00 KB _(~168 MB)_    | 172.11 B        |\n| **Trove**        | 26,563.00 KB                 | 26,563.00 KB                 | 272.01 B        | 1,425,995.00 KB _(~1.42 GB)_ | 1,460.22 B      |\n\n#### Why Scythe Dominates Memory & GC\n\n1. **Zero Per-Instance Allocations**: Maid, Janitor, and Trove allocate a separate Luau table (with metatables, hash bucket arrays, and wrapper closures) for every single scope and tracked object. This overhead costs **128 to 272 bytes per scope**. In contrast, a Scythe scope is an opaque integer (`u32` slot index + generation counter) costing **15.99 bytes per scope** in module-level SoA arrays.\n2. **0.00 KB Steady-State Garbage**: In high-frequency cleanup loops (e.g., combat hitboxes, projectiles, temporary VFX), OOP libraries continuously allocate and discard tables, flooding the Luau Garbage Collector with **160 MB to 1.4 GB of garbage across 1 million cycles**. Scythe recycles handle IDs via its `freeStack` buffer and retains warm buffer capacities (`scopeItems`, `scopeTags`), producing **0 bytes of heap garbage**.\n3. **No Frame Stutters**: By eliminating heap churn, Scythe ensures that high-frequency cleanup loops never trigger GC sweep pauses during gameplay.\n\n---\n\n### Collapsible Benchmark Suite\n\nClick any benchmark below to view the script, visual benchmark results, architectural analysis, and developer takeaways.\n\n<details>\n<summary><b>1. Scope Instantiation & Immediate Destruction</b> (<code>01_Instantiation.bench.luau</code>)</summary>\n\n<br>\n\n![](./assets/Bench01.png)\n\n- **Script**: [`benchmarks/01_Instantiation.bench.luau`](benchmarks/01_Instantiation.bench.luau)\n- **Description**: Measures the CPU cost of instantiating and immediately destroying 1,000 empty cleanup scopes/objects.\n- **Results (50% Median / Mode)**:\n    - **Trove**: `338.00 µs`\n    - **Janitor**: `784.00 µs`\n    - **Scythe**: `821.00 µs`\n    - **Maid**: `1302.00 µs`\n- **Why Scythe Stands Here**:\n    - Trove is fastest because `Trove.new()` simply allocates a bare Luau table (`{}`) without initializing tracking structures.\n    - Scythe (`821 µs`) is on par with Janitor (`784 µs`) and significantly faster than Maid (`1302 µs`). When allocating a new scope, Scythe unpacks integer generation handles and assigns initial SoA buffer slots.\n- **What It Means to the Developer**: Creating empty scopes that are instantly destroyed is rare in real code. However, Scythe's handle pooling guarantees that in steady-state gameplay, scope creation costs zero heap allocations.\n\n</details>\n\n<details>\n<summary><b>2. Adding & Disposing Functions</b> (<code>02_AddFunctions.bench.luau</code>)</summary>\n\n<br>\n\n![](./assets/Bench02.png)\n\n- **Script**: [`benchmarks/02_AddFunctions.bench.luau`](benchmarks/02_AddFunctions.bench.luau)\n- **Description**: Measures the efficiency of tracking and invoking 100 function cleanup callbacks per scope.\n- **Results (50% Median)**:\n    - **Janitor**: `15.00 µs`\n    - **Maid**: `17.00 µs`\n    - **Scythe**: `29.00 µs`\n    - **Trove**: `237.00 µs`\n- **Why Scythe Stands Here**:\n    - Janitor (`15 µs`) and Maid (`17 µs`) are faster here because adding a bare function to a table is an unvalidated `table.insert` / dictionary assignment.\n    - Scythe (`29 µs`) performs upfront type inspection at `add()` time, validates capacity, and writes to both the payload array and a contiguous `u8` tag buffer (`TAG_FUNCTION`). This ~12–14 µs upfront validation across 100 items allows the subsequent `clean()` loop to execute as a zero-branch integer read without calling `typeof()`.\n    - Trove (`237 µs`) is nearly 10× slower due to wrapper table allocations and checks during `Add()`.\n- **What It Means to the Developer**: Scythe trades a negligible microsecond validation cost at insertion time for maximum hot-loop safety and zero-overhead disposal.\n\n</details>\n\n<details>\n<summary><b>3. Adding & Disposing Connections</b> (<code>03_AddConnections.bench.luau</code>)</summary>\n\n<br>\n\n![](./assets/Bench03.png)\n\n- **Script**: [`benchmarks/03_AddConnections.bench.luau`](benchmarks/03_AddConnections.bench.luau)\n- **Description**: Measures tracking and disconnecting 100 mock event connections per scope.\n- **Results (50% Median)**:\n    - **Maid**: `22.00 µs`\n    - **Scythe**: `38.00 µs`\n    - **Trove**: `40.00 µs`\n    - **Janitor**: `44.00 µs`\n- **Why Scythe Stands Here**:\n    - Scythe (`38 µs`) outperforms both Trove (`40 µs`) and Janitor (`44 µs`).\n    - OOP libraries like Janitor and Trove require passing string method names (`\"Disconnect\"`) or wrapping connections, incurring string hash lookups and method sniffing. Scythe detects `RBXScriptConnection` automatically via `typeof()` at insertion time and stores `TAG_CONNECTION`, eliminating method strings and wrappers.\n    - Maid (`22 µs`) blind-inserts connections into a dictionary and defers type checking until cleanup time.\n- **What It Means to the Developer**: Event connections are tracked faster and more cleanly in Scythe - you never need to pass `\"Disconnect\"` strings or wrapper objects.\n\n</details>\n\n<details>\n<summary><b>4. Adding & Disposing Instances & Objects</b> (<code>04_AddDestroyables.bench.luau</code>)</summary>\n\n<br>\n\n![](./assets/Bench04.png)\n\n- **Script**: [`benchmarks/04_AddDestroyables.bench.luau`](benchmarks/04_AddDestroyables.bench.luau)\n- **Description**: Measures tracking and destroying 100 mock Instances / destroyable objects per scope.\n- **Results (50% Median)**:\n    - **Maid**: `35.00 µs`\n    - **Scythe**: `48.00 µs`\n    - **Trove**: `49.00 µs`\n    - **Janitor**: `57.00 µs`\n- **Why Scythe Stands Here**:\n    - Scythe (`48 µs`) outperforms both Trove (`49 µs`) and Janitor (`57 µs`).\n    - By pre-resolving the `:Destroy()` member once at `add()` time and caching `TAG_INSTANCE` or `TAG_DESTROY` in a `u8` buffer, Scythe avoids string method lookups during cleanup.\n- **What It Means to the Developer**: Managing parts, models, UI elements, and custom class instances is faster and safer in Scythe than in Janitor or Trove.\n\n</details>\n\n<details>\n<summary><b>5. Realistic Mixed Game Workload</b> (<code>05_MixedWorkload.bench.luau</code>)</summary>\n\n<br>\n\n![](./assets/Bench05.png)\n\n- **Script**: [`benchmarks/05_MixedWorkload.bench.luau`](benchmarks/05_MixedWorkload.bench.luau)\n- **Description**: Measures a realistic gameplay scenario tracking 200 mixed items per scope (functions, connections, instances, and threads) through full clean and destroy cycles.\n- **Results (50% Median)**:\n    - **Janitor**: `138.00 µs`\n    - **Scythe**: `161.00 µs`\n    - **Maid**: `171.00 µs`\n    - **Trove**: `234.00 µs`\n- **Why Scythe Stands Here**:\n    - In a realistic game workload mixing four different resource types, Scythe (`161 µs`) is on par with Janitor (`138 µs`) and noticeably faster than Maid (`171 µs`) and Trove (`234 µs`).\n    - Because Scythe stores precomputed disposal tags in contiguous `u8` buffers, its hot cleanup loop iterates across heterogeneous items without `typeof()` branches or method sniffing.\n- **What It Means to the Developer**: In practical game scripts with mixed tasks, connections, and instances, Scythe delivers top-tier execution speed without OOP overhead.\n\n</details>\n\n<details>\n<summary><b>6. Bulk Hot-Loop Cleanup</b> (<code>06_Clean1000.bench.luau</code>)</summary>\n\n<br>\n\n![](./assets/Bench06.png)\n\n- **Script**: [`benchmarks/06_Clean1000.bench.luau`](benchmarks/06_Clean1000.bench.luau)\n- **Description**: Measures hot-loop cleanup throughput when disposing a massive batch of 1,000 tracked items in a single scope.\n- **Results (50% Median)**:\n    - **Janitor**: `120.00 µs`\n    - **Maid**: `129.00 µs`\n    - **Scythe**: `167.00 µs`\n    - **Trove**: `2026.00 µs` _(>10× slower)_\n- **Why Scythe Stands Here**:\n    - Scythe (`167 µs`) cleans 1,000 items in a fraction of a millisecond, standing alongside Janitor (`120 µs`) and Maid (`129 µs`), while Trove collapses under bulk cleanup (`2026 µs`).\n    - Scythe pops items in LIFO order with per-item error isolation and re-entrancy protection, scanning `u8` tags without producing any GC garbage.\n- **What It Means to the Developer**: Heavy round resets, level transitions, and bulk entity removals execute instantaneously without causing GC frame drops.\n\n</details>\n\n<details>\n<summary><b>7. Steady-State Short-Lived Scopes</b> (<code>07_SteadyState.bench.luau</code>)</summary>\n\n<br>\n\n![](./assets/Bench07.png)\n\n- **Script**: [`benchmarks/07_SteadyState.bench.luau`](benchmarks/07_SteadyState.bench.luau)\n- **Description**: Measures handle pooling and buffer reuse across 100 rapid allocation and cleanup cycles (10 items each).\n- **Results (50% Median)**:\n    - **Janitor**: `202.00 µs`\n    - **Maid**: `210.00 µs`\n    - **Scythe**: `271.00 µs` _(~2.71 µs per cycle)_\n    - **Trove**: `2036.00 µs` _(>7× slower)_\n- **Why Scythe Stands Here**:\n    - In rapid steady-state churn, Scythe (`271 µs`) performs smoothly alongside Janitor (`202 µs`) and Maid (`210 µs`), while Trove suffers severe degradation (`2036 µs`).\n    - Scythe's handle pooling (`freeStack`) recycles scope IDs and retains warm buffer capacities, preventing heap allocation churn after warmup.\n- **What It Means to the Developer**: Perfect for high-frequency combat systems, projectiles, and temporary VFX scopes where constant scope creation must never trigger GC stutter.\n\n</details>\n\n<details>\n<summary><b>8. Selective Item Removal</b> (<code>08_SelectiveRemoval.bench.luau</code>)</summary>\n\n<br>\n\n![](./assets/Bench08.png)\n\n- **Script**: [`benchmarks/08_SelectiveRemoval.bench.luau`](benchmarks/08_SelectiveRemoval.bench.luau)\n- **Description**: Measures the performance of untracking 250 out of 500 items by value reference without triggering cleanup (`remove(scope, value)`).\n- **Results (50% Median)**:\n    - **Janitor**: `236.00 µs`\n    - **Trove**: `587.00 µs`\n    - **Scythe**: `1298.00 µs`\n- **Why Scythe Stands Here**:\n    - Janitor's `RemoveNoClean` takes an explicit dictionary index/key (`O(1)` hash lookup).\n    - Scythe's `remove(scope, value)` searches by **value reference** across a contiguous array using `rawequal` (`O(N)` linear scan per removal). Finding 250 items by value across a 500-item array requires linear scanning before performing `O(1)` swap-removal.\n- **What It Means to the Developer**: Untracking individual items by reference is `rawequal`-safe and swap-removed in `O(1)` once found. However, if your architecture requires untracking hundreds of items in a tight loop from a single scope, be aware that it performs an array search rather than a dictionary key lookup.\n\n</details>\n\n<details>\n<summary><b>9. Memory Allocation & GC Pressure</b> (<code>MemoryBenchmark.luau</code>)</summary>\n\n<br>\n\n![](./assets/Bench09.png)\n\n- **Script**: [`benchmarks/MemoryBenchmark.luau`](benchmarks/MemoryBenchmark.luau)\n- **Description**: Measures Peak Active RAM footprint (Cold and Warm starts across 100,000 items) and Total GC Garbage generated across 1,000,000 cleanup cycles.\n- **Results**:\n    - **Peak Active RAM (100k Items)**:\n        - **Scythe**: **`1,562.00 KB` (`15.99 Bytes per scope`)**\n        - **Janitor**: `12,500.","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/synttx/roblox/scythe/1.2.0/readme"}