From 4dec000904d0943c72b4b97f553848eaeaaf14eb Mon Sep 17 00:00:00 2001 From: Oliver Kucharzewski Date: Tue, 11 Nov 2025 14:48:30 +1100 Subject: [PATCH 1/2] feat: Add benchmarking capabilities and improve cache service - Added `benny` for benchmarking performance of cache reads and writes. - Replaced old benchmark tests with new structured benchmarks in `tests/benchmarks/read.ts` and `tests/benchmarks/write.ts`. - Updated `CacheService` to include a new method `mdel` for deleting multiple keys. - Introduced `CacheType` enum to categorize cache levels (memory, disk, distributed). - Enhanced `MemoryCacheLevel` and `RedisCacheLevel` to include `cacheType` property. - Refactored memory and Redis cache levels to support asynchronous operations in `set` and `updateStore` methods. - Removed outdated benchmark tests and utility functions, consolidating them into new benchmark files. - Updated `vitest.config.ts` to improve test coverage configuration. --- BENCHMARK.md | 249 ++------ benchmark/results/cache-read-performance.json | 27 + .../results/cache-read-performance.table.html | 25 + .../results/cache-write-performance.json | 27 + .../cache-write-performance.table.html | 25 + package-lock.json | 569 +++++++++++++++++- package.json | 4 +- src/cache.service.ts | 6 +- src/levels/interfaces/cache-level.ts | 7 + src/levels/interfaces/in-memory.ts | 2 + src/levels/memory/memory.level.ts | 12 +- src/levels/redis/redis.level.ts | 4 +- tests/benchmarks/benchmark.ts | 2 + tests/benchmarks/cache.benchmark.spec.ts | 291 --------- tests/benchmarks/read.ts | 76 +++ tests/benchmarks/write.ts | 69 +++ tests/utilities/benchmark-output.utilities.ts | 142 ----- tests/utilities/benchmark.utilities.ts | 170 ------ tests/utilities/data.utilities.ts | 13 + vitest.config.ts | 8 +- 20 files changed, 896 insertions(+), 832 deletions(-) create mode 100644 benchmark/results/cache-read-performance.json create mode 100644 benchmark/results/cache-read-performance.table.html create mode 100644 benchmark/results/cache-write-performance.json create mode 100644 benchmark/results/cache-write-performance.table.html create mode 100644 tests/benchmarks/benchmark.ts delete mode 100644 tests/benchmarks/cache.benchmark.spec.ts create mode 100644 tests/benchmarks/read.ts create mode 100644 tests/benchmarks/write.ts delete mode 100644 tests/utilities/benchmark-output.utilities.ts delete mode 100644 tests/utilities/benchmark.utilities.ts diff --git a/BENCHMARK.md b/BENCHMARK.md index 17bd996..9c93132 100644 --- a/BENCHMARK.md +++ b/BENCHMARK.md @@ -12,217 +12,38 @@ npm run benchmark The benchmarks use [Testcontainers](https://www.testcontainers.org/) to automatically spin up a Redis instance, so you don't need to have Redis running locally. -## Benchmark Suite - -The benchmark suite includes four comprehensive tests that measure different aspects of cache performance: - -### Benchmark 1: Cache Hit Rate Analysis - -**Purpose:** Measures how effectively the in-memory cache prevents Redis calls in a realistic scenario. - -**Methodology:** -- Executes 10,000 cache read operations -- Uses an 80/20 access pattern (80% of requests go to 20% of keys) which simulates real-world usage -- Tracks hits at the memory level vs. Redis level -- Measures cache misses - -**Key Metrics:** -- Memory cache hit rate -- Redis cache hit rate -- Cache miss rate -- Number of Redis calls prevented -- Performance metrics (latency, throughput) - -**Expected Results:** -- Memory cache should handle nearly 100% of requests for hot keys -- Demonstrates massive reduction in Redis network calls -- Shows sub-millisecond average latency - -**Example Output:** -``` -Results: - Total Calls: 10000 - Memory Cache Hits: 10000 (100.00%) - Redis Cache Hits: 0 (0.00%) - Cache Misses: 0 (0.00%) - - Performance Metrics: - Total Duration: 62ms - Average Latency: 0.01ms - P50 Latency: 0ms - P95 Latency: 0ms - P99 Latency: 0ms - Throughput: 161290.32 ops/sec - - Key Insights: - - Memory cache prevented 10000 Redis calls - - That's 100.00% reduction in Redis load - - Redis was hit 0 times when memory cache missed -``` - -### Benchmark 2: Speed Comparison - Multi-Level vs Redis-Only - -**Purpose:** Compares the overall performance of a multi-level cache (Memory + Redis) against a Redis-only cache. - -**Methodology:** -- Tests both cache configurations with 10,000 read operations -- Uses random access pattern across 100 unique keys -- Measures latency distribution (P50, P95, P99) -- Calculates throughput in operations per second - -**Key Metrics:** -- Total duration for all operations -- Average latency -- Latency percentiles (P50, P95, P99) -- Throughput (ops/sec) -- Performance improvement percentage - -**Expected Results:** -- Multi-level cache should be 90%+ faster than Redis-only -- Demonstrates the value of in-memory caching for frequently accessed data -- Shows orders of magnitude improvement in throughput - -**Example Output:** -``` -Multi-Level Cache Results: - Total Duration: 16ms - Avg Latency: 0.00ms - P50 Latency: 0ms - P95 Latency: 0ms - P99 Latency: 0ms - Throughput: 625000.00 ops/sec - -Redis-Only Cache Results: - Total Duration: 6965ms - Avg Latency: 0.70ms - P50 Latency: 1ms - P95 Latency: 1ms - P99 Latency: 2ms - Throughput: 1435.75 ops/sec - -Performance Comparison: - Multi-Level Cache is 99.77% FASTER overall - Multi-Level Cache has 99.80% LOWER average latency - Multi-Level Cache has 43431.25% HIGHER throughput -``` - -### Benchmark 3: Write Performance and Consistency - -**Purpose:** Measures write performance and documents the trade-off between read and write performance. - -**Methodology:** -- Executes 1,000 write operations for both cache types -- Measures write latency distribution -- Calculates write throughput - -**Key Metrics:** -- Write latency (average, P50, P95) -- Write throughput -- Performance comparison between multi-level and Redis-only - -**Expected Results:** -- Multi-level writes are slower (20-80%) than Redis-only -- This is expected as data must be written to both memory and Redis -- Documents the trade-off: slower writes for much faster reads - -**Example Output:** -``` -Multi-Level Cache Write Performance: - Total Duration: 9338ms - Avg Latency: 0.93ms - P50 Latency: 1ms - P95 Latency: 2ms - P99 Latency: 2ms - Throughput: 1070.89 ops/sec - -Redis-Only Cache Write Performance: - Total Duration: 5450ms - Avg Latency: 0.54ms - P50 Latency: 1ms - P95 Latency: 1ms - P99 Latency: 2ms - Throughput: 1834.86 ops/sec - -Write Performance Comparison: - Multi-Level writes are 71.34% SLOWER than Redis-only - This is expected as writes must update both memory and Redis layers -``` - -### Benchmark 4: Memory Efficiency Analysis - -**Purpose:** Analyzes memory usage and efficiency of the in-memory cache layer. - -**Methodology:** -- Populates cache with 100000 entries (~1KB each) -- Measures actual memory usage -- Calculates retention efficiency - -**Key Metrics:** -- Number of items in memory cache -- Estimated memory usage -- Average memory per item -- Memory efficiency (retention rate) - -**Expected Results:** -- Shows predictable memory usage based on cache size -- Demonstrates automatic eviction policy behavior -- Documents memory footprint for capacity planning - -**Example Output:** -``` -Memory Usage Statistics: - Items in Memory Cache: 10000 - Estimated Memory Usage: ~9.54 MB - Average Memory per Item: ~0.98 KB - Memory Efficiency: 100.00% of written items retained - -Memory Cache Benefits: - - Fast in-memory access for frequently accessed items - - Automatic eviction based on configured strategies - - Reduces network latency for cache hits - - Offloads Redis for better resource utilization -``` - -## Interpreting Results - -### When Multi-Level Caching Excels - -Multi-level caching provides the most benefit when: - -1. **High Read-to-Write Ratio:** Applications with more reads than writes benefit from fast in-memory access -2. **Hot Data Sets:** When a small subset of data is accessed frequently (e.g., 80/20 pattern) -3. **Latency-Sensitive Operations:** When sub-millisecond response times are critical -4. **High Concurrency:** When many operations need to be handled simultaneously - -### Trade-offs to Consider - -1. **Write Performance:** Writes are slower as data must be written to both cache levels -2. **Memory Usage:** In-memory cache consumes application memory -3. **Consistency:** Requires careful management in distributed systems -4. **Complexity:** Additional layer adds operational complexity - -## Performance Tips - -1. **Tune Memory Strategies:** Adjust `RamPercentageLimitStrategy` threshold based on your application's memory profile -2. **Choose Appropriate TTL:** Set cache TTL values that balance freshness and hit rate -3. **Monitor Cache Metrics:** Track hit rates to optimize cache configuration -4. **Size Your Cache:** Use Benchmark 4 to estimate memory requirements - -## System Requirements - -- Node.js 16+ -- Docker (for Testcontainers) -- At least 2GB RAM for running benchmarks - -## Contributing - -If you'd like to add additional benchmarks or improve existing ones, please: - -1. Follow the existing benchmark structure -2. Use Testcontainers for infrastructure dependencies -3. Document expected results and methodology -4. Ensure benchmarks are reproducible - -## License - -MIT License - See LICENSE file for details +## Benchmarks + +### Read Performance + +As expected, read performance for multi level cache is (99x) faster. This is due to the memory layer contributing to additional speed and being ready for each request. + + { + "name": "Multi-Level Cache", + "ops": 173913, + "margin": 6.93, + "percentSlower": 0 + }, + { + "name": "Redis-Only Cache", + "ops": 1621, + "margin": 7.28, + "percentSlower": 99.07 + } + +### Write Performance + +Multi level cache is 33% slower at the moment at writing as it is writing to memory. + + { + "name": "Multi-Level Cache", + "ops": 1002, + "margin": 6.6, + "percentSlower": 33.11 + }, + { + "name": "Redis-Only Cache", + "ops": 1498, + "margin": 9.15, + "percentSlower": 0 + } \ No newline at end of file diff --git a/benchmark/results/cache-read-performance.json b/benchmark/results/cache-read-performance.json new file mode 100644 index 0000000..d2b568e --- /dev/null +++ b/benchmark/results/cache-read-performance.json @@ -0,0 +1,27 @@ +{ + "name": "Cache Read Performance", + "date": "2025-11-11T01:31:28.741Z", + "version": null, + "results": [ + { + "name": "Multi-Level Cache", + "ops": 221896, + "margin": 6.48, + "percentSlower": 0 + }, + { + "name": "Redis-Only Cache", + "ops": 3676, + "margin": 6.48, + "percentSlower": 98.34 + } + ], + "fastest": { + "name": "Multi-Level Cache", + "index": 0 + }, + "slowest": { + "name": "Redis-Only Cache", + "index": 1 + } +} \ No newline at end of file diff --git a/benchmark/results/cache-read-performance.table.html b/benchmark/results/cache-read-performance.table.html new file mode 100644 index 0000000..1dde643 --- /dev/null +++ b/benchmark/results/cache-read-performance.table.html @@ -0,0 +1,25 @@ + + + + + + + Cache Read Performance + + + + + + + + + + + + + + + +
nameopsmarginpercentSlower
Multi-Level Cache1759304.710
Redis-Only Cache18258.8799.9
+ + \ No newline at end of file diff --git a/benchmark/results/cache-write-performance.json b/benchmark/results/cache-write-performance.json new file mode 100644 index 0000000..fb493d8 --- /dev/null +++ b/benchmark/results/cache-write-performance.json @@ -0,0 +1,27 @@ +{ + "name": "Cache Write Performance", + "date": "2025-11-11T03:40:53.610Z", + "version": null, + "results": [ + { + "name": "Multi-Level Cache", + "ops": 1002, + "margin": 6.6, + "percentSlower": 33.11 + }, + { + "name": "Redis-Only Cache", + "ops": 1498, + "margin": 9.15, + "percentSlower": 0 + } + ], + "fastest": { + "name": "Redis-Only Cache", + "index": 1 + }, + "slowest": { + "name": "Multi-Level Cache", + "index": 0 + } +} \ No newline at end of file diff --git a/benchmark/results/cache-write-performance.table.html b/benchmark/results/cache-write-performance.table.html new file mode 100644 index 0000000..3e2da53 --- /dev/null +++ b/benchmark/results/cache-write-performance.table.html @@ -0,0 +1,25 @@ + + + + + + + Cache Write Performance + + + + + + + + + + + + + + + +
nameopsmarginpercentSlower
Multi-Level Cache23016.320
Redis-Only Cache106641.8553.67
+ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 565c7b6..a0412c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@datastructures-js/heap": "^4.3.7", "@sesamecare-oss/redlock": "^1.4.0", + "benny": "^3.7.1", "json-with-bigint": "^3.4.4" }, "devDependencies": { @@ -18,6 +19,7 @@ "@testcontainers/redis": "^11.7.1", "@vitest/coverage-v8": "^3.2.4", "testcontainers": "^11.7.1", + "ts-node": "^10.9.2", "typescript": "^5.9.3", "vitest": "^3.2.4" }, @@ -39,6 +41,48 @@ "node": ">=6.0.0" } }, + "node_modules/@arrows/array": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz", + "integrity": "sha512-MGYS8xi3c4tTy1ivhrVntFvufoNzje0PchjEz6G/SsWRgUKxL4tKwS6iPdO8vsaJYldagAeWMd5KRD0aX3Q39g==", + "license": "ISC", + "dependencies": { + "@arrows/composition": "^1.2.2" + } + }, + "node_modules/@arrows/composition": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@arrows/composition/-/composition-1.2.2.tgz", + "integrity": "sha512-9fh1yHwrx32lundiB3SlZ/VwuStPB4QakPsSLrGJFH6rCXvdrd060ivAZ7/2vlqPnEjBkPRRXOcG1YOu19p2GQ==", + "license": "ISC" + }, + "node_modules/@arrows/dispatch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@arrows/dispatch/-/dispatch-1.0.3.tgz", + "integrity": "sha512-v/HwvrFonitYZM2PmBlAlCqVqxrkIIoiEuy5bQgn0BdfvlL0ooSBzcPzTMrtzY8eYktPyYcHg8fLbSgyybXEqw==", + "license": "ISC", + "dependencies": { + "@arrows/composition": "^1.2.2" + } + }, + "node_modules/@arrows/error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@arrows/error/-/error-1.0.2.tgz", + "integrity": "sha512-yvkiv1ay4Z3+Z6oQsUkedsQm5aFdyPpkBUQs8vejazU/RmANABx6bMMcBPPHI4aW43VPQmXFfBzr/4FExwWTEA==", + "license": "ISC" + }, + "node_modules/@arrows/multimethod": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@arrows/multimethod/-/multimethod-1.4.1.tgz", + "integrity": "sha512-AZnAay0dgPnCJxn3We5uKiB88VL+1ZIF2SjZohLj6vqY2UyvB/sKdDnFP+LZNVsTC5lcnGPmLlRRkAh4sXkXsQ==", + "license": "ISC", + "dependencies": { + "@arrows/array": "^1.4.1", + "@arrows/composition": "^1.2.2", + "@arrows/error": "^1.0.2", + "fast-deep-equal": "^3.1.3" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -269,6 +313,30 @@ "node": ">=14.21.3" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@datastructures-js/heap": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/@datastructures-js/heap/-/heap-4.3.7.tgz", @@ -1286,6 +1354,34 @@ "testcontainers": "^11.7.1" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", @@ -1542,6 +1638,47 @@ "node": ">=6.5" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1606,6 +1743,13 @@ "node": ">= 14" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -1638,6 +1782,15 @@ "js-tokens": "^9.0.1" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -1802,6 +1955,36 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/benchmark": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", + "integrity": "sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.4", + "platform": "^1.3.3" + } + }, + "node_modules/benny": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/benny/-/benny-3.7.1.tgz", + "integrity": "sha512-USzYxODdVfOS7JuQq/L0naxB788dWCiUgUTxvN+WLPt/JfcDURNNj8kN/N+uK6PDvuR67/9/55cVKGPleFQINA==", + "license": "ISC", + "dependencies": { + "@arrows/composition": "^1.0.0", + "@arrows/dispatch": "^1.0.2", + "@arrows/multimethod": "^1.1.6", + "benchmark": "^2.1.4", + "common-tags": "^1.8.0", + "fs-extra": "^10.0.0", + "json2csv": "^5.0.6", + "kleur": "^4.1.4", + "log-update": "^4.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -1963,6 +2146,18 @@ "dev": true, "license": "ISC" }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2071,7 +2266,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2084,9 +2278,26 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/compress-commons": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", @@ -2153,6 +2364,13 @@ "node": ">= 14" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2205,6 +2423,16 @@ "node": ">=0.10" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/docker-compose": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.3.0.tgz", @@ -2446,6 +2674,12 @@ "node": ">=12.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -2495,6 +2729,20 @@ "dev": true, "license": "MIT" }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2558,7 +2806,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -2635,7 +2882,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2751,6 +2997,55 @@ "integrity": "sha512-AhpYAAaZsPjU7smaBomDt1SOQshi9rEm6BlTbfVwsG1vNmeHKtEedJi62sHZzJTyKNtwzmNnrsd55kjwJ7054A==", "license": "MIT" }, + "node_modules/json2csv": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.7.tgz", + "integrity": "sha512-YRZbUnyaJZLZUJSRi2G/MqahCyRv9n/ds+4oIetjDF3jWQA7AG7iSeKTiZiCNqtMZM7HDyt0e/W6lEnoGEmMGA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "commander": "^6.1.0", + "jsonparse": "^1.3.1", + "lodash.get": "^4.4.2" + }, + "bin": { + "json2csv": "bin/json2csv.js" + }, + "engines": { + "node": ">= 10", + "npm": ">= 6.13.0" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -2801,7 +3096,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -2818,6 +3112,13 @@ "license": "MIT", "peer": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", @@ -2825,6 +3126,94 @@ "license": "MIT", "peer": true }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/log-update/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -2884,6 +3273,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2983,6 +3388,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -3054,6 +3474,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3245,6 +3671,25 @@ "node": ">=0.10.0" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -3381,6 +3826,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3758,6 +4235,50 @@ "node": ">=14.14" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -3765,6 +4286,18 @@ "dev": true, "license": "Unlicense" }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3796,6 +4329,15 @@ "dev": true, "license": "MIT" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3817,6 +4359,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.0.tgz", @@ -4223,6 +4772,16 @@ "node": ">=8" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", diff --git a/package.json b/package.json index 5178076..c3ae8b5 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "@datastructures-js/heap": "^4.3.7", "@sesamecare-oss/redlock": "^1.4.0", + "benny": "^3.7.1", "json-with-bigint": "^3.4.4" }, "devDependencies": { @@ -23,6 +24,7 @@ "@testcontainers/redis": "^11.7.1", "@vitest/coverage-v8": "^3.2.4", "testcontainers": "^11.7.1", + "ts-node": "^10.9.2", "typescript": "^5.9.3", "vitest": "^3.2.4" }, @@ -32,7 +34,7 @@ "scripts": { "test": "vitest --coverage", "test:debug": "vitest --inspect-brk --no-file-parallelism", - "benchmark": "vitest run tests/cache.benchmark.spec.ts", + "benchmark": "ts-node tests/benchmarks/cache-performance.ts", "typecheck": "tsc --noEmit", "build": "tsc", "lint": "biome check ./src", diff --git a/src/cache.service.ts b/src/cache.service.ts index ffc5ff0..945ada7 100644 --- a/src/cache.service.ts +++ b/src/cache.service.ts @@ -205,6 +205,10 @@ export class CacheService { return newValue as T; } + /** + * @description Loop through cache levels to delete the values for the given keys + * @param keys - cache keys + */ async mdel(keys: string[]): Promise { await Promise.allSettled( this.levels.map((level) => @@ -255,7 +259,7 @@ export class CacheService { await Promise.allSettled( this.levels.map((level) => { return handleGracefully( - () => level.set(key, value, ttl), + async () => level.set(key, value, ttl), "Failed to set key in cache level", ); }), diff --git a/src/levels/interfaces/cache-level.ts b/src/levels/interfaces/cache-level.ts index 6352c04..83d5be8 100644 --- a/src/levels/interfaces/cache-level.ts +++ b/src/levels/interfaces/cache-level.ts @@ -1,4 +1,11 @@ +export enum CacheType { + MEMORY = "memory", + DISK = "disk", + DISTRIBUTED = "distributed", +} + export interface CacheLevel { + cacheType: CacheType; /** * Store multiple values in the cache. * @param keys The cache keys. diff --git a/src/levels/interfaces/in-memory.ts b/src/levels/interfaces/in-memory.ts index 0ddf300..4008f84 100644 --- a/src/levels/interfaces/in-memory.ts +++ b/src/levels/interfaces/in-memory.ts @@ -1,6 +1,8 @@ import type { MemoryHeap } from "../../utils/heap.utils"; +import type { CacheType } from "./cache-level"; export interface InMemory { + cacheType: CacheType.MEMORY; /** * Get current memory usage as a percentage (entire system memory). * @return Percentage of memory used. diff --git a/src/levels/memory/memory.level.ts b/src/levels/memory/memory.level.ts index eb2af15..e13a818 100644 --- a/src/levels/memory/memory.level.ts +++ b/src/levels/memory/memory.level.ts @@ -4,7 +4,7 @@ import type { AbstractMemoryEvictionPolicy } from "../../policies/abstract/abstr import type { MemoryManagementStrategy } from "../../strategies/interfaces/memory-management-strategy"; import { createCacheHeap } from "../../utils/heap.utils"; import { serialize } from "../../utils/parsing.utils"; -import type { CacheLevel } from "../interfaces/cache-level"; +import { type CacheLevel, CacheType } from "../interfaces/cache-level"; import type { InMemory } from "../interfaces/in-memory"; import type { Purgable } from "../interfaces/purgable"; import { EvictionManager } from "./eviction-manager"; @@ -26,6 +26,8 @@ export interface MemoryLevelOptions { export class MemoryCacheLevel implements CacheLevel, Purgable, InMemory { + public cacheType: CacheType.MEMORY = CacheType.MEMORY; + protected store = new Map(); protected size = 0; protected heap = createCacheHeap((item) => item.expiry); @@ -43,7 +45,7 @@ export class MemoryCacheLevel await Promise.all(deletePromises); } - private updateStore(key: string, item: StoredItem) { + private async updateStore(key: string, item: StoredItem) { this.store.set(key, item); this.heap.insert({ ...item, key }); this.size += serialize(item).length; @@ -99,12 +101,12 @@ export class MemoryCacheLevel return cachedValue?.value as T; } - set(key: string, value: T, ttl: number = DEFAULT_TTL): Promise { + async set(key: string, value: T, ttl: number = DEFAULT_TTL): Promise { const expiryDate = Date.now() + ttl * 1000; const storedItem = { value, expiry: expiryDate }; - this.updateStore(key, storedItem); + await this.updateStore(key, storedItem); - return Promise.resolve(value as T); + return value as T; } async del(key: string): Promise { this.store.delete(key); diff --git a/src/levels/redis/redis.level.ts b/src/levels/redis/redis.level.ts index a73585c..55ecf1e 100644 --- a/src/levels/redis/redis.level.ts +++ b/src/levels/redis/redis.level.ts @@ -5,12 +5,14 @@ import { DEFAULT_TTL } from "../../constants"; import { parseIfJSON } from "../../utils/cache.utils"; import { deserialize, serialize } from "../../utils/parsing.utils"; import { generateVersionLookupKey } from "../../utils/version.utils"; -import type { CacheLevel } from "../interfaces/cache-level"; +import { type CacheLevel, CacheType } from "../interfaces/cache-level"; import type { Lockable } from "../interfaces/lockable"; export class RedisCacheLevel implements CacheLevel, Lockable { private client: IoRedis | Cluster; + public cacheType: CacheType = CacheType.DISTRIBUTED; + constructor(client: IoRedis | Cluster) { this.client = client; } diff --git a/tests/benchmarks/benchmark.ts b/tests/benchmarks/benchmark.ts new file mode 100644 index 0000000..afc77f2 --- /dev/null +++ b/tests/benchmarks/benchmark.ts @@ -0,0 +1,2 @@ +import './read'; +import './write'; diff --git a/tests/benchmarks/cache.benchmark.spec.ts b/tests/benchmarks/cache.benchmark.spec.ts deleted file mode 100644 index 2e37480..0000000 --- a/tests/benchmarks/cache.benchmark.spec.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { - RedisContainer, - type StartedRedisContainer, -} from "@testcontainers/redis"; -import { Redis } from "ioredis"; -import { afterAll, beforeAll, describe, it } from "vitest"; -import { CacheService } from "../../src/cache.service"; -import { - MemoryCacheLevel, - RedisCacheLevel, -} from "../../src/levels"; -import { FirstExpiringMemoryPolicy } from "../../src/policies/first-expiring-memory.policy"; -import { RamPercentageLimitStrategy } from "../../src/strategies/memory-percentage-limit.strategy"; -import type { StoredHeapItem } from "../../src/levels/memory/memory.level"; -import { - type BenchmarkResult, - calculateLatencyStats, - calculateThroughput, - get8020KeyIndex, - getUniformKeyIndex, - instrumentMemoryCache, - instrumentRedisCache, - populateCache, - runBenchmark, -} from "../utilities/benchmark.utilities"; -import { - printBenchmarkHeader, - printBenchmarkResults, - printCacheHitRateResults, - printMemoryEfficiency, - printPerformanceComparison, - printWritePerformanceComparison, -} from "../utilities/benchmark-output.utilities"; - -const TOTAL_CALLS = 10000; - -describe("Cache Performance Benchmarks", () => { - let redisContainer: StartedRedisContainer; - - beforeAll(async () => { - // Start Redis container - redisContainer = await new RedisContainer("redis:7.2").start(); - }, 60000); - - afterAll(async () => { - await redisContainer?.stop(); - }); - - it("Benchmark 1: Cache Hit Rate Analysis - 10,000 calls with 80/20 access pattern", async () => { - printBenchmarkHeader("BENCHMARK 1: CACHE HIT RATE ANALYSIS"); - - // Create fresh cache instances for this test - const redisClient = new Redis(redisContainer.getConnectionUrl()); - const memoryLevel = new MemoryCacheLevel({ - memoryStrategies: [new RamPercentageLimitStrategy(80)], - evictionPolicy: new FirstExpiringMemoryPolicy(), - }); - const redisLevel = new RedisCacheLevel(redisClient); - const multiLevelCache = new CacheService({ - levels: [memoryLevel, redisLevel], - defaultTTL: 3600, - }); - - // Pre-populate cache with data - await populateCache(multiLevelCache, "benchmark_key", TOTAL_CALLS); - - // Wait a bit to ensure data is properly set - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Set up hit/miss counters - const memoryCacheHits = { count: 0 }; - const redisCacheHits = { count: 0 }; - const cacheMisses = { count: 0 }; - - // Instrument cache levels to track hits - instrumentMemoryCache(memoryLevel, memoryCacheHits); - instrumentRedisCache(redisLevel, redisCacheHits, cacheMisses); - - // Run benchmark with 80/20 access pattern - const { latencies, totalDuration } = await runBenchmark(async () => { - const keyIndex = get8020KeyIndex(TOTAL_CALLS); - const key = `benchmark_key_${keyIndex}`; - await multiLevelCache.get(key, null); - }, TOTAL_CALLS); - - // Calculate statistics - const stats = calculateLatencyStats(latencies); - const throughput = calculateThroughput(TOTAL_CALLS, totalDuration); - - const result: BenchmarkResult = { - description: "Multi-Level Cache with 80/20 Access Pattern", - totalCalls: TOTAL_CALLS, - memoryCacheHits: memoryCacheHits.count, - redisCacheHits: redisCacheHits.count, - cacheMisses: cacheMisses.count, - totalDuration, - ...stats, - throughputOps: throughput, - }; - - // Print results - printCacheHitRateResults(result); - - // Cleanup - await redisClient.quit(); - }, 120000); - - it("Benchmark 2: Speed Comparison - Multi-Level vs Redis-Only", async () => { - printBenchmarkHeader("BENCHMARK 2: SPEED COMPARISON"); - - // Create fresh cache instances for this test - const redisClient = new Redis(redisContainer.getConnectionUrl()); - const redisOnlyClient = new Redis(redisContainer.getConnectionUrl()); - - const memoryLevel = new MemoryCacheLevel({ - memoryStrategies: [new RamPercentageLimitStrategy(80)], - evictionPolicy: new FirstExpiringMemoryPolicy(), - }); - const redisLevel = new RedisCacheLevel(redisClient); - const multiLevelCache = new CacheService({ - levels: [memoryLevel, redisLevel], - defaultTTL: 3600, - }); - - const redisOnlyLevel = new RedisCacheLevel(redisOnlyClient); - const redisOnlyCache = new CacheService({ - levels: [redisOnlyLevel], - defaultTTL: 3600, - }); - - const uniqueKeys = 100; - - // Pre-populate both caches - await populateCache(multiLevelCache, "speed_test_key", uniqueKeys); - await populateCache(redisOnlyCache, "speed_test_key", uniqueKeys); - - await new Promise((resolve) => setTimeout(resolve, 100)); - - // Benchmark Multi-Level Cache - console.log("Testing Multi-Level Cache (Memory + Redis)..."); - const multiLevelBenchmark = await runBenchmark(async () => { - const keyIndex = getUniformKeyIndex(uniqueKeys); - const key = `speed_test_key_${keyIndex}`; - await multiLevelCache.get(key, null); - }, TOTAL_CALLS); - - // Benchmark Redis-Only Cache - console.log("Testing Redis-Only Cache..."); - const redisOnlyBenchmark = await runBenchmark(async () => { - const keyIndex = getUniformKeyIndex(uniqueKeys); - const key = `speed_test_key_${keyIndex}`; - await redisOnlyCache.get(key, null); - }, TOTAL_CALLS); - - // Calculate statistics - const multiLevelStats = calculateLatencyStats(multiLevelBenchmark.latencies); - const redisOnlyStats = calculateLatencyStats(redisOnlyBenchmark.latencies); - - const multiLevelResult: BenchmarkResult = { - description: "Multi-Level Cache", - totalCalls: TOTAL_CALLS, - totalDuration: multiLevelBenchmark.totalDuration, - ...multiLevelStats, - throughputOps: calculateThroughput(TOTAL_CALLS, multiLevelBenchmark.totalDuration), - }; - - const redisOnlyResult: BenchmarkResult = { - description: "Redis-Only Cache", - totalCalls: TOTAL_CALLS, - totalDuration: redisOnlyBenchmark.totalDuration, - ...redisOnlyStats, - throughputOps: calculateThroughput(TOTAL_CALLS, redisOnlyBenchmark.totalDuration), - }; - - // Print comparison - printBenchmarkResults("Multi-Level Cache Results", multiLevelResult); - printBenchmarkResults("Redis-Only Cache Results", redisOnlyResult); - printPerformanceComparison(redisOnlyResult, multiLevelResult); - - // Cleanup - await redisClient.quit(); - await redisOnlyClient.quit(); - }, 120000); - - it("Benchmark 3: Write Performance and Consistency", async () => { - printBenchmarkHeader("BENCHMARK 3: WRITE PERFORMANCE"); - - // Create fresh cache instances for this test - const redisClient = new Redis(redisContainer.getConnectionUrl()); - const redisOnlyClient = new Redis(redisContainer.getConnectionUrl()); - - const memoryLevel = new MemoryCacheLevel({ - memoryStrategies: [new RamPercentageLimitStrategy(80)], - evictionPolicy: new FirstExpiringMemoryPolicy(), - }); - const redisLevel = new RedisCacheLevel(redisClient); - const multiLevelCache = new CacheService({ - levels: [memoryLevel, redisLevel], - defaultTTL: 3600, - }); - - const redisOnlyLevel = new RedisCacheLevel(redisOnlyClient); - const redisOnlyCache = new CacheService({ - levels: [redisOnlyLevel], - defaultTTL: 3600, - }); - - // Benchmark Multi-Level Cache Writes - console.log("Testing Multi-Level Cache writes..."); - const multiLevelBenchmark = await runBenchmark(async () => { - const i = Math.floor(Math.random() * TOTAL_CALLS); - const key = `write_test_key_${i}`; - const value = { id: i, data: `write_data_${i}_${"x".repeat(100)}` }; - await multiLevelCache.set(key, value); - }, TOTAL_CALLS); - - // Benchmark Redis-Only Cache Writes - console.log("Testing Redis-Only Cache writes..."); - const redisOnlyBenchmark = await runBenchmark(async () => { - const i = Math.floor(Math.random() * TOTAL_CALLS); - const key = `write_test_key_redis_${i}`; - const value = { id: i, data: `write_data_${i}_${"x".repeat(100)}` }; - await redisOnlyCache.set(key, value); - }, TOTAL_CALLS); - - // Calculate statistics - const multiLevelStats = calculateLatencyStats(multiLevelBenchmark.latencies); - const redisOnlyStats = calculateLatencyStats(redisOnlyBenchmark.latencies); - - const multiLevelResult: BenchmarkResult = { - description: "Multi-Level Cache", - totalCalls: TOTAL_CALLS, - totalDuration: multiLevelBenchmark.totalDuration, - ...multiLevelStats, - throughputOps: calculateThroughput(TOTAL_CALLS, multiLevelBenchmark.totalDuration), - }; - - const redisOnlyResult: BenchmarkResult = { - description: "Redis-Only Cache", - totalCalls: TOTAL_CALLS, - totalDuration: redisOnlyBenchmark.totalDuration, - ...redisOnlyStats, - throughputOps: calculateThroughput(TOTAL_CALLS, redisOnlyBenchmark.totalDuration), - }; - - // Print results - printBenchmarkResults("Multi-Level Cache Write Performance", multiLevelResult); - printBenchmarkResults("Redis-Only Cache Write Performance", redisOnlyResult); - printWritePerformanceComparison(multiLevelResult, redisOnlyResult); - - // Cleanup - await redisClient.quit(); - await redisOnlyClient.quit(); - }, 120000); - - it("Benchmark 4: Memory Efficiency Analysis", async () => { - printBenchmarkHeader("BENCHMARK 4: MEMORY EFFICIENCY"); - - // Create fresh cache instance for this test - const redisClient = new Redis(redisContainer.getConnectionUrl()); - const memoryLevel = new MemoryCacheLevel({ - memoryStrategies: [new RamPercentageLimitStrategy(80)], - evictionPolicy: new FirstExpiringMemoryPolicy(), - }); - const redisLevel = new RedisCacheLevel(redisClient); - const multiLevelCache = new CacheService({ - levels: [memoryLevel, redisLevel], - defaultTTL: 3600, - }); - const totalKeys = TOTAL_CALLS; - const valueSizeBytes = 1000; // ~1KB per value - - // Clear memory cache first - memoryLevel.purge(); - - console.log("Populating cache with test data..."); - await populateCache(multiLevelCache, "memory_test", totalKeys, valueSizeBytes); - - // Get memory usage info - const heap = memoryLevel.getHeap(); - const itemCount = heap.getCount(); - const estimatedMemoryMB = (itemCount * valueSizeBytes) / 1024 / 1024; - const avgMemoryPerItemKB = valueSizeBytes / 1024; - - // Print results - printMemoryEfficiency(itemCount, totalKeys, estimatedMemoryMB, avgMemoryPerItemKB); - - // Cleanup - await redisClient.quit(); - }, 120000); -}); diff --git a/tests/benchmarks/read.ts b/tests/benchmarks/read.ts new file mode 100644 index 0000000..2f29793 --- /dev/null +++ b/tests/benchmarks/read.ts @@ -0,0 +1,76 @@ +import benny from 'benny'; +import { + RedisContainer, + StartedRedisContainer, +} from '@testcontainers/redis'; +import { Redis } from 'ioredis'; +import { CacheService } from '../../src/cache.service'; +import { + MemoryCacheLevel, + RedisCacheLevel, +} from '../../src/levels'; +import { FirstExpiringMemoryPolicy } from '../../src/policies/first-expiring-memory.policy'; +import { RamPercentageLimitStrategy } from '../../src/strategies/ram-percentage-limit.strategy'; + +const TOTAL_CALLS = 10000; +let redisContainer: StartedRedisContainer; +let redisClient: Redis; +let redisOnlyClient: Redis; +let multiLevelCache: CacheService; +let redisOnlyCache: CacheService; + +async function setup() { + redisContainer = await new RedisContainer('redis:7.2').start(); + redisClient = new Redis(redisContainer.getConnectionUrl()); + redisOnlyClient = new Redis(redisContainer.getConnectionUrl()); + + const memoryLevel = new MemoryCacheLevel({ + memoryStrategies: [new RamPercentageLimitStrategy(80)], + evictionPolicy: new FirstExpiringMemoryPolicy(), + }); + const redisLevel = new RedisCacheLevel(redisClient); + multiLevelCache = new CacheService({ + levels: [memoryLevel, redisLevel], + defaultTTL: 3600, + }); + + const redisOnlyLevel = new RedisCacheLevel(redisOnlyClient); + redisOnlyCache = new CacheService({ + levels: [redisOnlyLevel], + defaultTTL: 3600, + }); + + // Pre-populate both caches + for (let i = 0; i < TOTAL_CALLS; i++) { + const key = `speed_test_key_${i}`; + const value = { id: i, data: `test_data_${i}` }; + await multiLevelCache.set(key, value); + await redisOnlyCache.set(key, value); + } +} + +async function teardown() { + await redisClient.quit(); + await redisOnlyClient.quit(); + await redisContainer.stop(); +} + +setup().then(() => { + benny.suite( + 'Cache Read Performance', + benny.add('Multi-Level Cache', async () => { + const keyIndex = Math.floor(Math.random() * TOTAL_CALLS); + const key = `speed_test_key_${keyIndex}`; + await multiLevelCache.get(key, null); + }), + benny.add('Redis-Only Cache', async () => { + const keyIndex = Math.floor(Math.random() * TOTAL_CALLS); + const key = `speed_test_key_${keyIndex}`; + await redisOnlyCache.get(key, null); + }), + benny.cycle(), + benny.complete(async () => { + await teardown(); + }), + ); +}); diff --git a/tests/benchmarks/write.ts b/tests/benchmarks/write.ts new file mode 100644 index 0000000..6b66f46 --- /dev/null +++ b/tests/benchmarks/write.ts @@ -0,0 +1,69 @@ +import benny from 'benny'; +import { + RedisContainer, + StartedRedisContainer, +} from '@testcontainers/redis'; +import { Redis } from 'ioredis'; +import { CacheService } from '../../src/cache.service'; +import { + MemoryCacheLevel, + RedisCacheLevel, +} from '../../src/levels'; +import { FirstExpiringMemoryPolicy } from '../../src/policies/first-expiring-memory.policy'; + +const TOTAL_CALLS = 10000; +let redisContainer: StartedRedisContainer; +let redisClient: Redis; +let redisOnlyClient: Redis; +let multiLevelCache: CacheService; +let redisOnlyCache: CacheService; +let memoryOnlyCache: MemoryCacheLevel; + +async function setup() { + redisContainer = await new RedisContainer('redis:7.2').start(); + redisClient = new Redis(redisContainer.getConnectionUrl()); + redisOnlyClient = new Redis(redisContainer.getConnectionUrl()); + + memoryOnlyCache = new MemoryCacheLevel({ + memoryStrategies: [], + evictionPolicy: new FirstExpiringMemoryPolicy(), + }); + const redisLevel = new RedisCacheLevel(redisClient); + multiLevelCache = new CacheService({ + levels: [memoryOnlyCache, redisLevel], + defaultTTL: 3600, + }); + + const redisOnlyLevel = new RedisCacheLevel(redisOnlyClient); + redisOnlyCache = new CacheService({ + levels: [redisOnlyLevel], + defaultTTL: 3600, + }); +} + +async function teardown() { + await redisClient.quit(); + await redisOnlyClient.quit(); + await redisContainer.stop(); +} + +setup().then(() => { + benny.suite( + 'Cache Write Performance', + benny.add('Multi-Level Cache', async () => { + const keyIndex = Math.floor(Math.random() * TOTAL_CALLS); + const key = `speed_test_key_${keyIndex}`; + await multiLevelCache.set(key, { id: keyIndex, data: `test_data_${keyIndex}` }); + }), + benny.add('Redis-Only Cache', async () => { + const keyIndex = Math.floor(Math.random() * TOTAL_CALLS); + const key = `speed_test_key_${keyIndex}`; + await redisOnlyCache.set(key, { id: keyIndex, data: `test_data_${keyIndex}` }); + }), + benny.cycle(), + benny.complete(async () => { + await teardown(); + }), + benny.save({ file: 'cache-write-performance', format: 'json' }) + ); +}); diff --git a/tests/utilities/benchmark-output.utilities.ts b/tests/utilities/benchmark-output.utilities.ts deleted file mode 100644 index 2e27415..0000000 --- a/tests/utilities/benchmark-output.utilities.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Utility functions for formatting and printing benchmark results - */ - -import type { BenchmarkResult } from "./benchmark.utilities"; - -/** - * Print cache hit rate analysis results - */ -export function printCacheHitRateResults(result: BenchmarkResult): void { - console.log("Results:"); - console.log(` Total Calls: ${result.totalCalls}`); - console.log( - ` Memory Cache Hits: ${result.memoryCacheHits} (${((result.memoryCacheHits! / result.totalCalls) * 100).toFixed(2)}%)`, - ); - console.log( - ` Redis Cache Hits: ${result.redisCacheHits} (${((result.redisCacheHits! / result.totalCalls) * 100).toFixed(2)}%)`, - ); - console.log( - ` Cache Misses: ${result.cacheMisses} (${((result.cacheMisses! / result.totalCalls) * 100).toFixed(2)}%)`, - ); - console.log("\n Performance Metrics:"); - console.log(` Total Duration: ${result.totalDuration}ms`); - console.log(` Average Latency: ${result.avgLatencyMs.toFixed(2)}ms`); - console.log(` P50 Latency: ${result.p50LatencyMs}ms`); - console.log(` P95 Latency: ${result.p95LatencyMs}ms`); - console.log(` P99 Latency: ${result.p99LatencyMs}ms`); - console.log(` Throughput: ${result.throughputOps.toFixed(2)} ops/sec`); - console.log("\n Key Insights:"); - console.log( - ` - Memory cache prevented ${result.memoryCacheHits} Redis calls`, - ); - console.log( - ` - That's ${((result.memoryCacheHits! / result.totalCalls) * 100).toFixed(2)}% reduction in Redis load`, - ); - console.log( - ` - Redis was hit ${result.redisCacheHits} times when memory cache missed\n`, - ); -} - -/** - * Print benchmark results for a single cache configuration - */ -export function printBenchmarkResults( - title: string, - result: BenchmarkResult, -): void { - console.log(`\n${title}:`); - console.log(` Total Duration: ${result.totalDuration}ms`); - console.log(` Avg Latency: ${result.avgLatencyMs.toFixed(2)}ms`); - console.log(` P50 Latency: ${result.p50LatencyMs}ms`); - console.log(` P95 Latency: ${result.p95LatencyMs}ms`); - console.log(` P99 Latency: ${result.p99LatencyMs}ms`); - console.log(` Throughput: ${result.throughputOps.toFixed(2)} ops/sec`); -} - -/** - * Print performance comparison between two benchmark results - */ -export function printPerformanceComparison( - baseline: BenchmarkResult, - improved: BenchmarkResult, -): void { - const speedImprovement = - ((baseline.totalDuration - improved.totalDuration) / - baseline.totalDuration) * - 100; - const latencyImprovement = - ((baseline.avgLatencyMs - improved.avgLatencyMs) / baseline.avgLatencyMs) * - 100; - const throughputImprovement = - ((improved.throughputOps - baseline.throughputOps) / - baseline.throughputOps) * - 100; - - console.log("\nPerformance Comparison:"); - console.log( - ` ${improved.description} is ${Math.abs(speedImprovement).toFixed(2)}% ${speedImprovement > 0 ? "FASTER" : "SLOWER"} overall`, - ); - console.log( - ` ${improved.description} has ${Math.abs(latencyImprovement).toFixed(2)}% ${latencyImprovement > 0 ? "LOWER" : "HIGHER"} average latency`, - ); - console.log( - ` ${improved.description} has ${Math.abs(throughputImprovement).toFixed(2)}% ${throughputImprovement > 0 ? "HIGHER" : "LOWER"} throughput\n`, - ); -} - -/** - * Print write performance comparison - */ -export function printWritePerformanceComparison( - multiLevel: BenchmarkResult, - redisOnly: BenchmarkResult, -): void { - const writeDiff = - ((multiLevel.totalDuration - redisOnly.totalDuration) / - redisOnly.totalDuration) * - 100; - - console.log("\nWrite Performance Comparison:"); - console.log( - ` Multi-Level writes are ${Math.abs(writeDiff).toFixed(2)}% ${writeDiff > 0 ? "SLOWER" : "FASTER"} than Redis-only`, - ); - console.log( - " This is expected as writes must update both memory and Redis layers\n", - ); -} - -/** - * Print memory efficiency statistics - */ -export function printMemoryEfficiency( - itemCount: number, - totalItems: number, - estimatedMemoryMB: number, - avgMemoryPerItemKB: number, -): void { - console.log("\nMemory Usage Statistics:"); - console.log(` Items in Memory Cache: ${itemCount}`); - console.log(` Estimated Memory Usage: ~${estimatedMemoryMB.toFixed(2)} MB`); - console.log( - ` Average Memory per Item: ~${avgMemoryPerItemKB.toFixed(2)} KB`, - ); - console.log( - ` Memory Efficiency: ${((itemCount / totalItems) * 100).toFixed(2)}% of written items retained`, - ); - - console.log("\nMemory Cache Benefits:"); - console.log(" - Fast in-memory access for frequently accessed items"); - console.log(" - Automatic eviction based on configured strategies"); - console.log(" - Reduces network latency for cache hits"); - console.log(" - Offloads Redis for better resource utilization\n"); -} - -/** - * Print benchmark section header - */ -export function printBenchmarkHeader(title: string): void { - console.log("\n========================================"); - console.log(title); - console.log("========================================\n"); -} diff --git a/tests/utilities/benchmark.utilities.ts b/tests/utilities/benchmark.utilities.ts deleted file mode 100644 index de59f12..0000000 --- a/tests/utilities/benchmark.utilities.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Utility functions for cache benchmarking - */ - -import type { MemoryCacheLevel } from "../../src/levels"; -import type { RedisCacheLevel } from "../../src/levels/redis/redis.level"; - -export interface BenchmarkResult { - description: string; - totalCalls: number; - memoryCacheHits?: number; - redisCacheHits?: number; - cacheMisses?: number; - totalDuration: number; - avgLatencyMs: number; - p50LatencyMs: number; - p95LatencyMs: number; - p99LatencyMs: number; - throughputOps: number; -} - -export interface LatencyStats { - avgLatencyMs: number; - p50LatencyMs: number; - p95LatencyMs: number; - p99LatencyMs: number; -} - -/** - * Calculate latency statistics from an array of latency measurements - */ -export function calculateLatencyStats(latencies: number[]): LatencyStats { - const sortedLatencies = [...latencies].sort((a, b) => a - b); - const avgLatency = - sortedLatencies.reduce((a, b) => a + b, 0) / sortedLatencies.length; - const p50 = sortedLatencies[Math.floor(sortedLatencies.length * 0.5)]; - const p95 = sortedLatencies[Math.floor(sortedLatencies.length * 0.95)]; - const p99 = sortedLatencies[Math.floor(sortedLatencies.length * 0.99)]; - - return { - avgLatencyMs: avgLatency, - p50LatencyMs: p50, - p95LatencyMs: p95, - p99LatencyMs: p99, - }; -} - -/** - * Calculate throughput in operations per second - */ -export function calculateThroughput( - totalCalls: number, - totalDurationMs: number, -): number { - return (totalCalls / totalDurationMs) * 1000; -} - -/** - * Calculate percentage improvement between two values - */ -export function calculateImprovement(baseline: number, improved: number): number { - return ((baseline - improved) / baseline) * 100; -} - -/** - * Instrument a memory cache level to track hits - */ -export function instrumentMemoryCache( - memoryLevel: MemoryCacheLevel, - hitCounter: { count: number }, -): void { - const originalGet = memoryLevel.get.bind(memoryLevel); - const instrumentedGet = async function ( - key: string, - valueGetter?: (() => Promise) | T, - ttl?: number, - ): Promise { - const result = await originalGet(key); - if (result !== null && result !== undefined) { - hitCounter.count++; - } - return result as T; - }; - memoryLevel.get = instrumentedGet as any; -} - -/** - * Instrument a Redis cache level to track hits and misses - */ -export function instrumentRedisCache( - redisLevel: RedisCacheLevel, - hitCounter: { count: number }, - missCounter: { count: number }, -): void { - const originalGet = redisLevel.get.bind(redisLevel); - const instrumentedGet = async function ( - key: string, - valueGetter?: (() => Promise) | T, - ttl?: number, - ): Promise { - const result = await originalGet(key); - if (result !== null && result !== undefined) { - hitCounter.count++; - } else { - missCounter.count++; - } - return result as T; - }; - redisLevel.get = instrumentedGet as any; -} - -/** - * Generate a random key index following an 80/20 access pattern - * 80% of requests go to 20% of the keys (hot keys) - */ -export function get8020KeyIndex(totalKeys: number, hotKeyRatio = 0.2): number { - const hotKeys = Math.floor(totalKeys * hotKeyRatio); - if (Math.random() < 0.8) { - // 80% of requests go to hot keys - return Math.floor(Math.random() * hotKeys); - } - // 20% of requests go to cold keys - return hotKeys + Math.floor(Math.random() * (totalKeys - hotKeys)); -} - -/** - * Generate a random key index with uniform distribution - */ -export function getUniformKeyIndex(totalKeys: number): number { - return Math.floor(Math.random() * totalKeys); -} - -/** - * Populate cache with test data - */ -export async function populateCache Promise }>( - cache: T, - keyPrefix: string, - count: number, - valueSize = 100, -): Promise { - for (let i = 0; i < count; i++) { - const key = `${keyPrefix}_${i}`; - const value = { id: i, data: `test_data_${i}_${"x".repeat(valueSize)}` }; - await cache.set(key, value); - } -} - -/** - * Run a benchmark with latency tracking - */ -export async function runBenchmark( - operationFn: () => Promise, - iterations: number, -): Promise<{ latencies: number[]; totalDuration: number }> { - const latencies: number[] = []; - const startTime = Date.now(); - - for (let i = 0; i < iterations; i++) { - const callStart = Date.now(); - await operationFn(); - const callEnd = Date.now(); - latencies.push(callEnd - callStart); - } - - const endTime = Date.now(); - const totalDuration = endTime - startTime; - - return { latencies, totalDuration }; -} diff --git a/tests/utilities/data.utilities.ts b/tests/utilities/data.utilities.ts index d1abd0b..fc85b94 100644 --- a/tests/utilities/data.utilities.ts +++ b/tests/utilities/data.utilities.ts @@ -1,6 +1,19 @@ import { faker } from "@faker-js/faker"; import type { CacheLevel } from "../../src/levels/interfaces/cache-level"; +export async function populateCache Promise }>( + cache: T, + keyPrefix: string, + count: number, + valueSize = 100, +): Promise { + for (let i = 0; i < count; i++) { + const key = `${keyPrefix}_${i}`; + const value = { id: i, data: `test_data_${i}_${"x".repeat(valueSize)}` }; + await cache.set(key, value); + } +} + export async function generateJSONData( cacheLevel: CacheLevel, recordNum: number, diff --git a/vitest.config.ts b/vitest.config.ts index 5238aea..f6206fa 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from "vitest/config"; -export default defineConfig({ +export const config = { test: { coverage: { exclude: [ @@ -12,5 +12,9 @@ export default defineConfig({ ], }, exclude: ["**/benchmarks/**", "node_modules/**"], - }, + } +}; + +export default defineConfig({ + ...config, }); From a779f81b6d13e2d21c5346693bd29d8f09f8c4f0 Mon Sep 17 00:00:00 2001 From: Oliver Kucharzewski Date: Tue, 11 Nov 2025 14:49:07 +1100 Subject: [PATCH 2/2] 1.3.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0412c3..bf7e257 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cacheforge", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cacheforge", - "version": "1.2.0", + "version": "1.3.0", "dependencies": { "@datastructures-js/heap": "^4.3.7", "@sesamecare-oss/redlock": "^1.4.0", diff --git a/package.json b/package.json index c3ae8b5..1773039 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cacheforge", - "version": "1.2.0", + "version": "1.3.0", "description": "A multi-level caching library for Node.js applications, supporting in-memory and Redis, and custom cache levels.", "main": "dist/src/index.js", "types": "dist/src/index.d.ts",