From e96873be73aaa509462501b3cdc85d340043c23d Mon Sep 17 00:00:00 2001 From: Mohamed Date: Sat, 21 Jun 2025 00:33:36 +0300 Subject: [PATCH 01/14] feat(benchmark): add comprehensive benchmarking suite for qs library - Implemented parsing benchmarks for various query string formats including simple, nested, and array queries. - Added stringifying benchmarks for different object structures and array formats. - Created a runner to manage benchmark execution and results reporting. - Introduced fixtures for generating test data patterns for benchmarking. - Added functionality to compare current results against baseline benchmarks. - Implemented options for saving current results as baseline and generating summary reports. - Enhanced command-line interface for running benchmarks with various options. --- benchmark/README.md | 206 ++++++++++++++++++ .../results/benchmark-1750454342309.json | 169 ++++++++++++++ .../results/benchmark-1750454442152.json | 160 ++++++++++++++ .../results/benchmark-1750454596215.json | 169 ++++++++++++++ benchmark/fixtures.js | 129 +++++++++++ benchmark/index.js | 175 +++++++++++++++ benchmark/package.json | 19 ++ benchmark/parse.js | 109 +++++++++ benchmark/runner.js | 167 ++++++++++++++ benchmark/stringify.js | 93 ++++++++ 10 files changed, 1396 insertions(+) create mode 100644 benchmark/README.md create mode 100644 benchmark/benchmark/results/benchmark-1750454342309.json create mode 100644 benchmark/benchmark/results/benchmark-1750454442152.json create mode 100644 benchmark/benchmark/results/benchmark-1750454596215.json create mode 100644 benchmark/fixtures.js create mode 100644 benchmark/index.js create mode 100644 benchmark/package.json create mode 100644 benchmark/parse.js create mode 100644 benchmark/runner.js create mode 100644 benchmark/stringify.js diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 00000000..5a64d275 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,206 @@ +# QS Library Benchmarking Suite + +This directory contains a comprehensive benchmarking infrastructure for the `qs` library, designed to measure performance across different Node.js versions and detect performance regressions. + +## Features + +- πŸš€ **Comprehensive Test Coverage**: Parse and stringify operations across various scenarios +- πŸ“Š **Cross-Node Version Testing**: Automated testing on Node.js 16, 18, 20, and 22 +- πŸ“ˆ **Regression Detection**: Compare performance against baselines +- πŸ”„ **CI Integration**: Automated benchmarks on every PR and push +- πŸ“‹ **Detailed Reporting**: Visual tables with performance metrics +- πŸ’Ύ **Historical Tracking**: Track performance trends over time + +## Quick Start + +### Install Dependencies +```bash +cd benchmark +npm install +``` + +### Run All Benchmarks +```bash +# Run all benchmarks +node index.js + +# Run only parse benchmarks +node index.js --parse + +# Run only stringify benchmarks +node index.js --stringify +``` + +### Compare with Baseline +```bash +# Save current results as baseline +node index.js --save-baseline + +# Compare against baseline +node index.js --baseline baseline.json +``` + +## Test Scenarios + +### Parse Benchmarks +- **Simple queries**: `a=1&b=2&c=3` +- **Nested objects**: `user[profile][name]=John` +- **Arrays**: `colors[]=red&colors[]=green` +- **Real-world cases**: E-commerce, forms, APIs +- **Edge cases**: Empty values, special characters +- **Different options**: `allowDots`, `comma`, `depth` + +### Stringify Benchmarks +- **Simple objects**: `{a: '1', b: '2'}` +- **Nested objects**: `{user: {name: 'John'}}` +- **Arrays**: `{colors: ['red', 'green']}` +- **Array formats**: indices, brackets, repeat, comma +- **Options**: `allowDots`, encoding, delimiters +- **Edge cases**: null values, empty arrays + +## Performance Metrics + +Each benchmark reports: +- **Operations per second (Hz)**: Primary performance metric +- **Relative margin of error (RME)**: Statistical reliability +- **Sample size**: Number of test runs +- **Fastest/Slowest indicators**: Comparative performance + +## Example Output + +``` +πŸ“Š Benchmark Results + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Test Name β”‚ Ops/sec β”‚ RME β”‚ Samples β”‚ Status β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Parse simple tiny query β”‚ 2,451,234 β”‚ Β±1.23% β”‚ 89 β”‚ πŸš€ Fastest β”‚ +β”‚ Parse simple small query β”‚ 1,876,543 β”‚ Β±0.87% β”‚ 92 β”‚ β”‚ +β”‚ Parse nested object β”‚ 923,456 β”‚ Β±1.45% β”‚ 85 β”‚ β”‚ +β”‚ Parse complex array β”‚ 654,321 β”‚ Β±2.12% β”‚ 78 β”‚ 🐌 Slowest β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Baseline Comparison + +When comparing against a baseline: + +``` +πŸ“ˆ Performance Comparison + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Test Name β”‚ Current β”‚ Baseline β”‚ Change β”‚ Status β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Parse simple query β”‚ 2,451,234 β”‚ 2,234,567 β”‚ +9.70% β”‚ βœ… Improved β”‚ +β”‚ Parse nested object β”‚ 923,456 β”‚ 934,123 β”‚ -1.14% β”‚ βž– No changeβ”‚ +β”‚ Parse complex array β”‚ 654,321 β”‚ 724,891 β”‚ -9.73% β”‚ ⚠️ Regressionβ”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Command Line Options + +```bash +node benchmark/index.js [options] + +Options: + --parse Run only parse benchmarks + --stringify Run only stringify benchmarks + --baseline FILE Compare results against baseline file + --save-baseline Save current results as baseline + --no-summary Skip summary report + --help Show help + +Environment Variables: + STRESS_TEST=1 Include stress test scenarios +``` + +## Adding New Benchmarks + +### 1. Add Test Data +Edit `fixtures.js` to add new test scenarios: + +```javascript +const newScenarios = { + myNewTest: 'complex=query&string=here' +}; +``` + +### 2. Add Benchmark +In `parse.js` or `stringify.js`: + +```javascript +runner.add('My new test case', () => { + qs.parse(fixtures.newScenarios.myNewTest); +}); +``` + +### 3. Test Locally +```bash +node index.js --parse +``` + +## Performance Guidelines + +### What to Benchmark +- βœ… Core functionality (parse/stringify) +- βœ… Common real-world scenarios +- βœ… Edge cases that might be slow +- βœ… Different option combinations +- βœ… Large inputs (stress tests) + +### What NOT to Benchmark +- ❌ Trivial operations +- ❌ Error conditions (use unit tests) +- ❌ Platform-specific features +- ❌ Operations that vary by environment + +### Interpreting Results + +- **5%+ improvement**: Significant performance gain βœ… +- **Β±5% change**: No meaningful change βž– +- **5%+ regression**: Potential performance issue ⚠️ + +## Troubleshooting + +### Inconsistent Results +- Ensure system is not under load +- Run multiple times and average +- Check for background processes + +### Memory Issues +- Use `--max-old-space-size=4096` for large tests +- Monitor memory usage during stress tests + +### CI Failures +- Check Node version compatibility +- Verify all dependencies are installed +- Review benchmark timeout settings + +## File Structure + +``` +benchmark/ +β”œβ”€β”€ index.js # Main benchmark runner +β”œβ”€β”€ runner.js # Benchmark infrastructure +β”œβ”€β”€ fixtures.js # Test data generators +β”œβ”€β”€ parse.js # Parse function benchmarks +β”œβ”€β”€ stringify.js # Stringify function benchmarks +β”œβ”€β”€ package.json # Benchmark dependencies +β”œβ”€β”€ README.md # This documentation +└── results/ # Generated benchmark results + β”œβ”€β”€ benchmark-*.json + └── baseline.json +``` + +## Contributing + +When adding performance optimizations: + +1. **Run baseline first**: `node index.js --save-baseline` +2. **Make your changes**: Implement optimization +3. **Run comparison**: `node index.js --baseline baseline.json` +4. **Verify improvements**: Check for positive performance changes +5. **Include stress tests**: `STRESS_TEST=1 node index.js` + +This ensures your optimization actually improves performance and doesn't introduce regressions. + diff --git a/benchmark/benchmark/results/benchmark-1750454342309.json b/benchmark/benchmark/results/benchmark-1750454342309.json new file mode 100644 index 00000000..7b832316 --- /dev/null +++ b/benchmark/benchmark/results/benchmark-1750454342309.json @@ -0,0 +1,169 @@ +{ + "timestamp": "2025-06-20T21:19:02.296Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Parse simple tiny query", + "hz": 697833.6386936898, + "rme": 1.1681832751356365, + "mean": 0.000001433006299140223, + "sample": 91, + "fastest": true, + "slowest": false + }, + { + "name": "Parse simple small query", + "hz": 333344.8868012807, + "rme": 0.7665510509001657, + "mean": 0.0000029998960223923796, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple medium query", + "hz": 81681.29623992334, + "rme": 0.6887321102570698, + "mean": 0.000012242704830034644, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple large query", + "hz": 16183.406763108791, + "rme": 0.8353154746767609, + "mean": 0.0000617916866725225, + "sample": 95, + "fastest": false, + "slowest": true + }, + { + "name": "Parse shallow nested", + "hz": 331721.95409273804, + "rme": 0.7740676253490404, + "mean": 0.0000030145728603794324, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Parse medium nested", + "hz": 262336.0057169703, + "rme": 0.4314241603618106, + "mean": 0.0000038119052596953937, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Parse deep nested", + "hz": 144619.34728908024, + "rme": 0.44536020425504225, + "mean": 0.000006914704143983555, + "sample": 99, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple array", + "hz": 374633.2818277549, + "rme": 0.7104402811137674, + "mean": 0.0000026692769930135833, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse indexed array", + "hz": 220048.48148864487, + "rme": 0.49671738413420585, + "mean": 0.000004544453082497653, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse mixed array/object", + "hz": 157740.04879441197, + "rme": 0.5853969792354998, + "mean": 0.000006339544127460836, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse e-commerce query", + "hz": 112051.98143502424, + "rme": 0.5159663283577665, + "mean": 0.000008924429422784207, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse form data", + "hz": 131241.05545123873, + "rme": 0.2051087777285156, + "mean": 0.000007619566884476479, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse API query", + "hz": 102799.06016443495, + "rme": 0.3642870811382813, + "mean": 0.000009727715393510638, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse empty values", + "hz": 508054.16454990895, + "rme": 0.23030272252997103, + "mean": 0.0000019682940713337357, + "sample": 100, + "fastest": false, + "slowest": false + }, + { + "name": "Parse special characters", + "hz": 666303.7335275853, + "rme": 0.170597791898354, + "mean": 0.0000015008170443616455, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with allowDots", + "hz": 387799.56779563095, + "rme": 0.24773325732867316, + "mean": 0.0000025786516619507854, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with comma arrays", + "hz": 639742.9214602712, + "rme": 0.6495100337107264, + "mean": 0.0000015631278853659048, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with depth limit", + "hz": 153594.02605669567, + "rme": 0.5349662303802443, + "mean": 0.0000065106698852393725, + "sample": 94, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750454442152.json b/benchmark/benchmark/results/benchmark-1750454442152.json new file mode 100644 index 00000000..d56fc7bb --- /dev/null +++ b/benchmark/benchmark/results/benchmark-1750454442152.json @@ -0,0 +1,160 @@ +{ + "timestamp": "2025-06-20T21:20:42.149Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Stringify simple object", + "hz": 2037374.6865790153, + "rme": 0.38175901075425256, + "mean": 4.908277336453582e-7, + "sample": 96, + "fastest": true, + "slowest": false + }, + { + "name": "Stringify nested object", + "hz": 408556.066063155, + "rme": 0.3368748760920129, + "mean": 0.000002447644480318202, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify arrays", + "hz": 226212.74356404794, + "rme": 0.8662747505196786, + "mean": 0.0000044206174428757084, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify mixed object", + "hz": 117138.94402626439, + "rme": 0.749993217517359, + "mean": 0.00000853687053705883, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify large object", + "hz": 28825.879879177846, + "rme": 0.38975236367677246, + "mean": 0.00003469104860602511, + "sample": 96, + "fastest": false, + "slowest": true + }, + { + "name": "Stringify array indices format", + "hz": 219437.76757235164, + "rme": 0.7290565006242969, + "mean": 0.000004557100680812779, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify array brackets format", + "hz": 226129.31509867168, + "rme": 0.3911649465251547, + "mean": 0.0000044222483916499255, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify array repeat format", + "hz": 250559.12014171766, + "rme": 0.48659264259497126, + "mean": 0.0000039910740404675525, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify array comma format", + "hz": 572028.3255381505, + "rme": 0.2674909038397102, + "mean": 0.0000017481651788121228, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with allowDots", + "hz": 423312.84029828827, + "rme": 0.47859322788251846, + "mean": 0.000002362319081309577, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify without encoding", + "hz": 314276.9997364952, + "rme": 0.5185917828975549, + "mean": 0.000003181906410072794, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with query prefix", + "hz": 1540447.7269144836, + "rme": 0.44489478007002803, + "mean": 6.491619173621683e-7, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with custom delimiter", + "hz": 1198322.2475114788, + "rme": 0.3984677658832, + "mean": 8.345000704749253e-7, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify null values", + "hz": 1798516.1840537034, + "rme": 0.38608776984646054, + "mean": 5.560139012739294e-7, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with strictNullHandling", + "hz": 1319198.5442423075, + "rme": 0.32916194615612765, + "mean": 7.58036009336531e-7, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with skipNulls", + "hz": 1532309.748620865, + "rme": 0.2201108754463058, + "mean": 6.52609565983664e-7, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify special characters", + "hz": 1232357.2098995054, + "rme": 0.36282680440289805, + "mean": 8.114530364792093e-7, + "sample": 96, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750454596215.json b/benchmark/benchmark/results/benchmark-1750454596215.json new file mode 100644 index 00000000..06b7f871 --- /dev/null +++ b/benchmark/benchmark/results/benchmark-1750454596215.json @@ -0,0 +1,169 @@ +{ + "timestamp": "2025-06-20T21:23:16.201Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Parse simple tiny query", + "hz": 723258.9943741269, + "rme": 0.35483449296383623, + "mean": 0.0000013826305760156515, + "sample": 95, + "fastest": true, + "slowest": false + }, + { + "name": "Parse simple small query", + "hz": 355150.7402551628, + "rme": 0.1424851978448738, + "mean": 0.0000028157058022222806, + "sample": 100, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple medium query", + "hz": 84104.35454009412, + "rme": 0.39088580594264205, + "mean": 0.000011889990779529511, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple large query", + "hz": 16509.73850456047, + "rme": 0.276763071980723, + "mean": 0.00006057031125742971, + "sample": 96, + "fastest": false, + "slowest": true + }, + { + "name": "Parse shallow nested", + "hz": 350204.28068824776, + "rme": 0.4422855453635856, + "mean": 0.0000028554762324284697, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse medium nested", + "hz": 259764.0327765841, + "rme": 0.11239456732731663, + "mean": 0.000003849647656417748, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse deep nested", + "hz": 145215.76333611264, + "rme": 0.10667275458392099, + "mean": 0.00000688630474424065, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple array", + "hz": 393278.25699955504, + "rme": 0.10987173378822206, + "mean": 0.00000254272892589923, + "sample": 100, + "fastest": false, + "slowest": false + }, + { + "name": "Parse indexed array", + "hz": 234055.33468120755, + "rme": 0.4110444970997853, + "mean": 0.000004272493944058309, + "sample": 90, + "fastest": false, + "slowest": false + }, + { + "name": "Parse mixed array/object", + "hz": 164416.117203606, + "rme": 0.32599376184516793, + "mean": 0.000006082128790096911, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse e-commerce query", + "hz": 118759.72830911238, + "rme": 0.47318027941807034, + "mean": 0.000008420362813538623, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse form data", + "hz": 134391.6156726525, + "rme": 0.3497367447128347, + "mean": 0.000007440940381547115, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse API query", + "hz": 105333.06194387515, + "rme": 0.12438374842192938, + "mean": 0.000009493695346413002, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse empty values", + "hz": 509933.3278528553, + "rme": 0.18355617829640952, + "mean": 0.000001961040679985829, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Parse special characters", + "hz": 677648.6347977747, + "rme": 0.4228916585233394, + "mean": 0.00000147569101249414, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with allowDots", + "hz": 382126.8096919969, + "rme": 0.10508550735949418, + "mean": 0.0000026169323236075042, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with comma arrays", + "hz": 664503.237195686, + "rme": 0.1220931753822807, + "mean": 0.0000015048835641797112, + "sample": 101, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with depth limit", + "hz": 156745.76615928378, + "rme": 0.0899787948667524, + "mean": 0.000006379757645152648, + "sample": 101, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/fixtures.js b/benchmark/fixtures.js new file mode 100644 index 00000000..2af2310c --- /dev/null +++ b/benchmark/fixtures.js @@ -0,0 +1,129 @@ +'use strict'; + +/** + * Generates various test data patterns for benchmarking + */ + +// Simple flat query strings +const simpleQueries = { + tiny: 'a=1&b=2&c=3', + small: 'name=John&age=30&city=NYC&country=USA&email=john@example.com', + medium: Array.from({length: 20}, (_, i) => `param${i}=value${i}`).join('&'), + large: Array.from({length: 100}, (_, i) => `param${i}=value${i}`).join('&'), + xlarge: Array.from({length: 500}, (_, i) => `param${i}=value${i}`).join('&') +}; + +// Nested object queries +const nestedQueries = { + shallow: 'user[name]=John&user[age]=30&user[email]=john@example.com', + medium: 'user[profile][name]=John&user[profile][bio]=Developer&user[settings][theme]=dark', + deep: 'user[profile][personal][name][first]=John&user[profile][personal][name][last]=Doe&user[profile][personal][address][street]=123 Main St&user[profile][personal][address][city]=NYC', + veryDeep: 'a[b][c][d][e][f][g][h][i][j]=deep' +}; + +// Array queries +const arrayQueries = { + simple: 'colors[]=red&colors[]=green&colors[]=blue', + indexed: 'items[0]=first&items[1]=second&items[2]=third&items[3]=fourth&items[4]=fifth', + sparse: 'sparse[1]=second&sparse[5]=sixth&sparse[10]=tenth', + mixed: 'user[name]=John&user[hobbies][]=reading&user[hobbies][]=coding&user[addresses][0][street]=123 Main&user[addresses][0][city]=NYC', + large: Array.from({length: 50}, (_, i) => `items[]=${i}`).join('&') +}; + +// Edge case queries +const edgeCaseQueries = { + emptyValues: 'a=&b=&c=value&d=', + specialChars: 'special=%21%40%23%24%25%5E%26%2A%28%29&unicode=%E2%9C%93', + longValues: `long=${'x'.repeat(1000)}&short=y`, + manyParams: Array.from({length: 200}, (_, i) => `p${i}=v${i}`).join('&'), + deepNesting: Array.from({length: 10}, (_, i) => `level${i}`).reduce((acc, level) => `${acc}[${level}]`, 'deep') + '=value' +}; + +// Mixed complexity queries (realistic scenarios) +const realWorldQueries = { + ecommerce: 'category=electronics&subcategory=phones&brand[]=apple&brand[]=samsung&price[min]=100&price[max]=1000&features[]=camera&features[]=5g&sort=price&order=asc&page=2&limit=20', + form: 'user[name]=John%20Doe&user[email]=john%40example.com&user[age]=30&preferences[newsletter]=true&preferences[theme]=dark&address[street]=123%20Main%20St&address[city]=New%20York&address[zip]=10001', + analytics: 'event=pageview×tamp=1640995200&user[id]=12345&user[session]=abc123&page[url]=/products/123&page[title]=Product%20Details&utm[source]=google&utm[medium]=cpc&utm[campaign]=summer_sale', + api: 'fields[]=id&fields[]=name&fields[]=email&include[]=profile&include[]=settings&filter[status]=active&filter[created_after]=2023-01-01&sort[]=name&sort[]=-created_at&page[number]=1&page[size]=25' +}; + +// Performance stress tests +const stressTestQueries = { + manyKeys: Array.from({length: 1000}, (_, i) => `key${i}=value${i}`).join('&'), + deepNesting: 'a' + '[level]'.repeat(20) + '=deep', + largeArrays: Array.from({length: 100}, (_, i) => `items[]=${i}`).join('&'), + mixedComplexity: [ + ...Array.from({length: 50}, (_, i) => `simple${i}=value${i}`), + ...Array.from({length: 20}, (_, i) => `nested[${i}][prop]=value${i}`), + ...Array.from({length: 30}, (_, i) => `array[]=${i}`) + ].join('&') +}; + +// Generate test objects for stringify benchmarks +const testObjects = { + simple: { a: '1', b: '2', c: '3' }, + nested: { + user: { + name: 'John', + profile: { + age: 30, + bio: 'Developer' + } + } + }, + arrays: { + colors: ['red', 'green', 'blue'], + numbers: [1, 2, 3, 4, 5] + }, + mixed: { + name: 'John', + hobbies: ['reading', 'coding'], + address: { + street: '123 Main St', + city: 'NYC', + coordinates: { + lat: 40.7128, + lng: -74.0060 + } + }, + preferences: { + theme: 'dark', + notifications: { + email: true, + push: false + } + } + }, + large: Object.fromEntries( + Array.from({length: 100}, (_, i) => [`key${i}`, `value${i}`]) + ) +}; + +module.exports = { + simpleQueries, + nestedQueries, + arrayQueries, + edgeCaseQueries, + realWorldQueries, + stressTestQueries, + testObjects, + + // Helper functions + generateQuery: (type, size = 'medium') => { + const queries = { + simple: simpleQueries, + nested: nestedQueries, + array: arrayQueries, + edge: edgeCaseQueries, + real: realWorldQueries, + stress: stressTestQueries + }; + + return queries[type]?.[size] || simpleQueries.medium; + }, + + generateObject: (type) => { + return testObjects[type] || testObjects.simple; + } +}; + diff --git a/benchmark/index.js b/benchmark/index.js new file mode 100644 index 00000000..5bfc72c5 --- /dev/null +++ b/benchmark/index.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node +'use strict'; + +const runParseBenchmarks = require('./parse'); +const runStringifyBenchmarks = require('./stringify'); +const chalk = require('chalk'); +const fs = require('fs'); +const path = require('path'); + +async function main() { + const args = process.argv.slice(2); + const options = parseArgs(args); + + console.log(chalk.blue.bold('πŸš€ QS Library Benchmarks')); + console.log(chalk.gray(`Node.js ${process.version} on ${process.platform}-${process.arch}\n`)); + + if (options.baseline) { + console.log(chalk.yellow(`πŸ“Š Comparing against baseline: ${options.baseline}\n`)); + } + + try { + if (options.parse || options.all) { + await runParseBenchmarks(); + console.log('\n' + '='.repeat(60) + '\n'); + } + + if (options.stringify || options.all) { + await runStringifyBenchmarks(); + console.log('\n' + '='.repeat(60) + '\n'); + } + + // Generate summary report + if (options.summary) { + generateSummaryReport(); + } + + console.log(chalk.green('βœ… Benchmarks completed successfully!')); + + if (options.save) { + saveBaseline(); + } + + } catch (error) { + console.error(chalk.red('❌ Benchmark failed:'), error.message); + process.exit(1); + } +} + +function parseArgs(args) { + const options = { + all: true, + parse: false, + stringify: false, + baseline: null, + summary: true, + save: false + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + switch (arg) { + case '--parse': + options.parse = true; + options.all = false; + break; + case '--stringify': + options.stringify = true; + options.all = false; + break; + case '--baseline': + options.baseline = args[++i]; + break; + case '--save-baseline': + options.save = true; + break; + case '--no-summary': + options.summary = false; + break; + case '--help': + printHelp(); + process.exit(0); + default: + if (arg.startsWith('--')) { + console.error(chalk.red(`Unknown option: ${arg}`)); + printHelp(); + process.exit(1); + } + } + } + + return options; +} + +function printHelp() { + console.log(` +${chalk.blue.bold('QS Benchmark Suite')} + +${chalk.yellow('Usage:')} + node benchmark/index.js [options] + +${chalk.yellow('Options:')} + --parse Run only parse benchmarks + --stringify Run only stringify benchmarks + --baseline FILE Compare results against baseline file + --save-baseline Save current results as baseline + --no-summary Skip summary report + --help Show this help + +${chalk.yellow('Examples:')} + node benchmark/index.js # Run all benchmarks + node benchmark/index.js --parse # Run only parse benchmarks + node benchmark/index.js --baseline baseline.json # Compare with baseline + node benchmark/index.js --save-baseline # Save as new baseline + STRESS_TEST=1 node benchmark/index.js # Include stress tests + +${chalk.yellow('Environment Variables:')} + STRESS_TEST=1 Include stress test scenarios +`); +} + +function generateSummaryReport() { + const resultsDir = path.join(__dirname, 'results'); + if (!fs.existsSync(resultsDir)) return; + + const files = fs.readdirSync(resultsDir) + .filter(f => f.startsWith('benchmark-') && f.endsWith('.json')) + .sort() + .slice(-5); // Last 5 runs + + if (files.length === 0) return; + + console.log(chalk.blue('\nπŸ“ˆ Performance Trend (Last 5 Runs)\n')); + + const trends = {}; + files.forEach(file => { + const data = JSON.parse(fs.readFileSync(path.join(resultsDir, file), 'utf8')); + data.results.forEach(result => { + if (!trends[result.name]) trends[result.name] = []; + trends[result.name].push(result.hz); + }); + }); + + Object.entries(trends).forEach(([testName, values]) => { + if (values.length < 2) return; + + const latest = values[values.length - 1]; + const previous = values[values.length - 2]; + const change = ((latest - previous) / previous * 100).toFixed(2); + const trend = change > 5 ? 'πŸ“ˆ' : change < -5 ? 'πŸ“‰' : 'βž–'; + + console.log(`${trend} ${testName}: ${change >= 0 ? '+' : ''}${change}%`); + }); +} + +function saveBaseline() { + const resultsDir = path.join(__dirname, 'results'); + const files = fs.readdirSync(resultsDir) + .filter(f => f.startsWith('benchmark-') && f.endsWith('.json')) + .sort(); + + if (files.length === 0) return; + + const latest = files[files.length - 1]; + const baselinePath = path.join(__dirname, 'baseline.json'); + + fs.copyFileSync(path.join(resultsDir, latest), baselinePath); + console.log(chalk.green(`πŸ’Ύ Baseline saved to ${baselinePath}`)); +} + +if (require.main === module) { + main().catch(console.error); +} + +module.exports = { main }; + diff --git a/benchmark/package.json b/benchmark/package.json new file mode 100644 index 00000000..42560010 --- /dev/null +++ b/benchmark/package.json @@ -0,0 +1,19 @@ +{ + "name": "qs-benchmarks", + "private": true, + "description": "Benchmarking suite for qs library", + "scripts": { + "benchmark": "cd benchmark && npm install && node index.js", + "benchmark:parse": "cd benchmark && npm install && node index.js --parse", + "benchmark:stringify": "cd benchmark && npm install && node index.js --stringify", + "benchmark:baseline": "cd benchmark && npm install && node index.js --save-baseline", + "benchmark:compare": "cd benchmark && npm install && node index.js --baseline baseline.json", + "benchmark:stress": "cd benchmark && npm install && STRESS_TEST=1 node index.js" + }, + "dependencies": { + "benchmark": "^2.1.4", + "table": "^6.8.1", + "chalk": "^4.1.2" + } +} + diff --git a/benchmark/parse.js b/benchmark/parse.js new file mode 100644 index 00000000..69cc28a2 --- /dev/null +++ b/benchmark/parse.js @@ -0,0 +1,109 @@ +'use strict'; + +const BenchmarkRunner = require('./runner'); +const fixtures = require('./fixtures'); +const qs = require('../lib/index'); + +async function runParseBenchmarks() { + console.log('πŸ” Running Parse Benchmarks...\n'); + + const runner = new BenchmarkRunner({ + verbose: true, + outputDir: './benchmark/results' + }); + + // Simple parsing benchmarks + runner + .add('Parse simple tiny query', () => { + qs.parse(fixtures.simpleQueries.tiny); + }) + .add('Parse simple small query', () => { + qs.parse(fixtures.simpleQueries.small); + }) + .add('Parse simple medium query', () => { + qs.parse(fixtures.simpleQueries.medium); + }) + .add('Parse simple large query', () => { + qs.parse(fixtures.simpleQueries.large); + }); + + // Nested object parsing + runner + .add('Parse shallow nested', () => { + qs.parse(fixtures.nestedQueries.shallow); + }) + .add('Parse medium nested', () => { + qs.parse(fixtures.nestedQueries.medium); + }) + .add('Parse deep nested', () => { + qs.parse(fixtures.nestedQueries.deep); + }); + + // Array parsing + runner + .add('Parse simple array', () => { + qs.parse(fixtures.arrayQueries.simple); + }) + .add('Parse indexed array', () => { + qs.parse(fixtures.arrayQueries.indexed); + }) + .add('Parse mixed array/object', () => { + qs.parse(fixtures.arrayQueries.mixed); + }); + + // Real-world scenarios + runner + .add('Parse e-commerce query', () => { + qs.parse(fixtures.realWorldQueries.ecommerce); + }) + .add('Parse form data', () => { + qs.parse(fixtures.realWorldQueries.form); + }) + .add('Parse API query', () => { + qs.parse(fixtures.realWorldQueries.api); + }); + + // Edge cases + runner + .add('Parse empty values', () => { + qs.parse(fixtures.edgeCaseQueries.emptyValues); + }) + .add('Parse special characters', () => { + qs.parse(fixtures.edgeCaseQueries.specialChars); + }); + + // Options testing + runner + .add('Parse with allowDots', () => { + qs.parse('user.name=John&user.age=30', { allowDots: true }); + }) + .add('Parse with comma arrays', () => { + qs.parse('colors=red,green,blue', { comma: true }); + }) + .add('Parse with depth limit', () => { + qs.parse(fixtures.nestedQueries.deep, { depth: 2 }); + }); + + // Stress tests (comment out for regular benchmarks) + if (process.env.STRESS_TEST) { + runner + .add('Parse many keys (stress)', () => { + qs.parse(fixtures.stressTestQueries.manyKeys); + }) + .add('Parse deep nesting (stress)', () => { + qs.parse(fixtures.stressTestQueries.deepNesting); + }) + .add('Parse large arrays (stress)', () => { + qs.parse(fixtures.stressTestQueries.largeArrays); + }); + } + + await runner.run(); +} + +if (require.main === module) { + runParseBenchmarks().catch(console.error); +} + +module.exports = runParseBenchmarks; + diff --git a/benchmark/runner.js b/benchmark/runner.js new file mode 100644 index 00000000..54523979 --- /dev/null +++ b/benchmark/runner.js @@ -0,0 +1,167 @@ +'use strict'; + +const Benchmark = require('benchmark'); +const { table } = require('table'); +const chalk = require('chalk'); +const fs = require('fs'); +const path = require('path'); + +class BenchmarkRunner { + constructor(options = {}) { + this.options = { + outputDir: options.outputDir || path.join(__dirname, 'results'), + baseline: options.baseline || null, + verbose: options.verbose || false, + ...options + }; + this.results = []; + this.suite = new Benchmark.Suite(); + + // Ensure output directory exists + if (!fs.existsSync(this.options.outputDir)) { + fs.mkdirSync(this.options.outputDir, { recursive: true }); + } + } + + add(name, fn, options = {}) { + this.suite.add(name, fn, { + onComplete: (event) => { + const benchmark = event.target; + this.results.push({ + name: benchmark.name, + hz: benchmark.hz, + rme: benchmark.stats.rme, + mean: benchmark.stats.mean, + sample: benchmark.stats.sample.length, + fastest: false, + slowest: false + }); + }, + ...options + }); + return this; + } + + async run() { + return new Promise((resolve, reject) => { + this.suite + .on('start', () => { + if (this.options.verbose) { + console.log(chalk.blue('πŸš€ Starting benchmarks...')); + } + }) + .on('cycle', (event) => { + if (this.options.verbose) { + console.log(chalk.gray(String(event.target))); + } + }) + .on('complete', () => { + this._markFastestSlowest(); + this._generateReport(); + resolve(this.results); + }) + .on('error', reject) + .run({ async: true }); + }); + } + + _markFastestSlowest() { + if (this.results.length === 0) return; + + const sorted = [...this.results].sort((a, b) => b.hz - a.hz); + sorted[0].fastest = true; + sorted[sorted.length - 1].slowest = true; + } + + _generateReport() { + const timestamp = new Date().toISOString(); + const nodeVersion = process.version; + const platform = `${process.platform}-${process.arch}`; + + // Console output + this._printConsoleReport(); + + // JSON output for CI/regression detection + const jsonReport = { + timestamp, + nodeVersion, + platform, + results: this.results + }; + + const jsonPath = path.join(this.options.outputDir, `benchmark-${Date.now()}.json`); + fs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2)); + + // Baseline comparison if available + if (this.options.baseline) { + this._compareWithBaseline(jsonReport); + } + } + + _printConsoleReport() { + console.log(chalk.green('\nπŸ“Š Benchmark Results\n')); + + const tableData = [ + ['Test Name', 'Ops/sec', 'RME', 'Samples', 'Status'] + ]; + + this.results.forEach(result => { + const opsPerSec = result.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }); + const rme = `Β±${result.rme.toFixed(2)}%`; + const status = result.fastest ? 'πŸš€ Fastest' : + result.slowest ? '🐌 Slowest' : ''; + + tableData.push([ + result.name, + opsPerSec, + rme, + result.sample.toString(), + status + ]); + }); + + console.log(table(tableData)); + } + + _compareWithBaseline(currentReport) { + if (!fs.existsSync(this.options.baseline)) { + console.log(chalk.yellow('⚠️ Baseline file not found, skipping comparison')); + return; + } + + try { + const baseline = JSON.parse(fs.readFileSync(this.options.baseline, 'utf8')); + console.log(chalk.blue('\nπŸ“ˆ Performance Comparison\n')); + + const comparisonData = [ + ['Test Name', 'Current', 'Baseline', 'Change', 'Status'] + ]; + + currentReport.results.forEach(current => { + const baselineResult = baseline.results.find(b => b.name === current.name); + if (!baselineResult) return; + + const change = ((current.hz - baselineResult.hz) / baselineResult.hz * 100).toFixed(2); + const changeStr = `${change >= 0 ? '+' : ''}${change}%`; + const status = Math.abs(change) < 5 ? 'βž– No change' : + change > 5 ? 'βœ… Improved' : + '⚠️ Regression'; + + comparisonData.push([ + current.name, + current.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }), + baselineResult.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }), + changeStr, + status + ]); + }); + + console.log(table(comparisonData)); + } catch (error) { + console.log(chalk.red(`❌ Error comparing with baseline: ${error.message}`)); + } + } +} + +module.exports = BenchmarkRunner; + diff --git a/benchmark/stringify.js b/benchmark/stringify.js new file mode 100644 index 00000000..47cffa9e --- /dev/null +++ b/benchmark/stringify.js @@ -0,0 +1,93 @@ +'use strict'; + +const BenchmarkRunner = require('./runner'); +const fixtures = require('./fixtures'); +const qs = require('../lib/index'); + +async function runStringifyBenchmarks() { + console.log('πŸ”— Running Stringify Benchmarks...\n'); + + const runner = new BenchmarkRunner({ + verbose: true, + outputDir: './benchmark/results' + }); + + // Simple object stringifying + runner + .add('Stringify simple object', () => { + qs.stringify(fixtures.testObjects.simple); + }) + .add('Stringify nested object', () => { + qs.stringify(fixtures.testObjects.nested); + }) + .add('Stringify arrays', () => { + qs.stringify(fixtures.testObjects.arrays); + }) + .add('Stringify mixed object', () => { + qs.stringify(fixtures.testObjects.mixed); + }) + .add('Stringify large object', () => { + qs.stringify(fixtures.testObjects.large); + }); + + // Different array formats + runner + .add('Stringify array indices format', () => { + qs.stringify(fixtures.testObjects.arrays, { arrayFormat: 'indices' }); + }) + .add('Stringify array brackets format', () => { + qs.stringify(fixtures.testObjects.arrays, { arrayFormat: 'brackets' }); + }) + .add('Stringify array repeat format', () => { + qs.stringify(fixtures.testObjects.arrays, { arrayFormat: 'repeat' }); + }) + .add('Stringify array comma format', () => { + qs.stringify(fixtures.testObjects.arrays, { arrayFormat: 'comma' }); + }); + + // Different options + runner + .add('Stringify with allowDots', () => { + qs.stringify(fixtures.testObjects.nested, { allowDots: true }); + }) + .add('Stringify without encoding', () => { + qs.stringify(fixtures.testObjects.mixed, { encode: false }); + }) + .add('Stringify with query prefix', () => { + qs.stringify(fixtures.testObjects.simple, { addQueryPrefix: true }); + }) + .add('Stringify with custom delimiter', () => { + qs.stringify(fixtures.testObjects.simple, { delimiter: ';' }); + }); + + // Edge cases + const edgeObjects = { + nullValues: { a: null, b: '', c: 'value' }, + undefinedValues: { a: undefined, b: 'value', c: null }, + emptyArrays: { arr: [], obj: {}, value: 'test' }, + specialChars: { special: '!@#$%^&*()', unicode: 'βœ“' } + }; + + runner + .add('Stringify null values', () => { + qs.stringify(edgeObjects.nullValues); + }) + .add('Stringify with strictNullHandling', () => { + qs.stringify(edgeObjects.nullValues, { strictNullHandling: true }); + }) + .add('Stringify with skipNulls', () => { + qs.stringify(edgeObjects.nullValues, { skipNulls: true }); + }) + .add('Stringify special characters', () => { + qs.stringify(edgeObjects.specialChars); + }); + + await runner.run(); +} + +if (require.main === module) { + runStringifyBenchmarks().catch(console.error); +} + +module.exports = runStringifyBenchmarks; + From 843f2e9ffeff440dd7d984420ec6d4e83331dbb0 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Sat, 21 Jun 2025 01:42:15 +0300 Subject: [PATCH 02/14] fix(package): ignore benchmark directory in publishConfig --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 78bdae36..1a4ace26 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "publishConfig": { "ignore": [ "!dist/*", + "benchmark", "bower.json", "component.json", ".github/workflows", From 683bd4b4897326e1bc5e092ddb55a274cbbddac1 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Sat, 21 Jun 2025 01:42:57 +0300 Subject: [PATCH 03/14] refactor: replace chalk with colors --- .../results/benchmark-1750458974767.json | 169 ++++++++++++++++++ .../results/benchmark-1750459074889.json | 160 +++++++++++++++++ benchmark/index.js | 28 +-- benchmark/package.json | 2 +- benchmark/runner.js | 14 +- 5 files changed, 351 insertions(+), 22 deletions(-) create mode 100644 benchmark/benchmark/results/benchmark-1750458974767.json create mode 100644 benchmark/benchmark/results/benchmark-1750459074889.json diff --git a/benchmark/benchmark/results/benchmark-1750458974767.json b/benchmark/benchmark/results/benchmark-1750458974767.json new file mode 100644 index 00000000..dac61366 --- /dev/null +++ b/benchmark/benchmark/results/benchmark-1750458974767.json @@ -0,0 +1,169 @@ +{ + "timestamp": "2025-06-20T22:36:14.754Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Parse simple tiny query", + "hz": 741706.0976391918, + "rme": 0.291338734691978, + "mean": 0.0000013482429269261004, + "sample": 99, + "fastest": true, + "slowest": false + }, + { + "name": "Parse simple small query", + "hz": 353993.26933922776, + "rme": 0.39354178718805893, + "mean": 0.0000028249124675918946, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple medium query", + "hz": 82378.43106603576, + "rme": 0.30148925498931284, + "mean": 0.000012139099847609203, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple large query", + "hz": 16668.393821329242, + "rme": 0.4482299400778523, + "mean": 0.000059993782887489625, + "sample": 89, + "fastest": false, + "slowest": true + }, + { + "name": "Parse shallow nested", + "hz": 346343.1409931819, + "rme": 0.1502699708357385, + "mean": 0.0000028873099583620335, + "sample": 99, + "fastest": false, + "slowest": false + }, + { + "name": "Parse medium nested", + "hz": 269390.6093815785, + "rme": 0.13230828389767865, + "mean": 0.0000037120818810114847, + "sample": 100, + "fastest": false, + "slowest": false + }, + { + "name": "Parse deep nested", + "hz": 150858.36475971554, + "rme": 0.3992477649667113, + "mean": 0.000006628734187811076, + "sample": 99, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple array", + "hz": 394986.18072817277, + "rme": 0.27325821232522357, + "mean": 0.000002531734143600822, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse indexed array", + "hz": 227041.01456683056, + "rme": 0.5890672990218405, + "mean": 0.000004404490536249103, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse mixed array/object", + "hz": 164424.52635209722, + "rme": 0.3700866902750369, + "mean": 0.000006081817732341274, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse e-commerce query", + "hz": 116458.2283425666, + "rme": 0.4105686178223879, + "mean": 0.000008586769816371063, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse form data", + "hz": 130247.8388317256, + "rme": 0.18328636288465622, + "mean": 0.00000767767057764356, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse API query", + "hz": 105523.63100473923, + "rme": 0.23687452866920514, + "mean": 0.000009476550327907958, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse empty values", + "hz": 517989.8737175659, + "rme": 0.4010984920826836, + "mean": 0.000001930539670250871, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse special characters", + "hz": 660999.1463362356, + "rme": 0.25922009614695707, + "mean": 0.0000015128612579044424, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with allowDots", + "hz": 383837.1475119348, + "rme": 0.18513550518740124, + "mean": 0.000002605271549359111, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with comma arrays", + "hz": 657057.0874023354, + "rme": 0.14373804238904503, + "mean": 0.0000015219377724901864, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with depth limit", + "hz": 156256.49658681112, + "rme": 0.2981065395867695, + "mean": 0.000006399733910867712, + "sample": 95, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750459074889.json b/benchmark/benchmark/results/benchmark-1750459074889.json new file mode 100644 index 00000000..0293c910 --- /dev/null +++ b/benchmark/benchmark/results/benchmark-1750459074889.json @@ -0,0 +1,160 @@ +{ + "timestamp": "2025-06-20T22:37:54.886Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Stringify simple object", + "hz": 2024182.530416778, + "rme": 0.46962465183362023, + "mean": 4.940265934387353e-7, + "sample": 92, + "fastest": true, + "slowest": false + }, + { + "name": "Stringify nested object", + "hz": 408904.35155663424, + "rme": 0.16901447303840778, + "mean": 0.000002445559691876983, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify arrays", + "hz": 242431.34212288828, + "rme": 0.329612211801757, + "mean": 0.0000041248791977280756, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify mixed object", + "hz": 123572.12030502668, + "rme": 0.4262874350371448, + "mean": 0.000008092440248913669, + "sample": 90, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify large object", + "hz": 28745.95525484991, + "rme": 0.2561594313882339, + "mean": 0.00003478750283768301, + "sample": 96, + "fastest": false, + "slowest": true + }, + { + "name": "Stringify array indices format", + "hz": 220190.31018409316, + "rme": 0.46934638208201307, + "mean": 0.000004541525915304521, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify array brackets format", + "hz": 233458.89117637026, + "rme": 0.4558774545584227, + "mean": 0.00000428340936154166, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify array repeat format", + "hz": 256700.1583741599, + "rme": 0.40762738534430965, + "mean": 0.000003895595570854399, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify array comma format", + "hz": 561467.7912818117, + "rme": 0.15926460902620682, + "mean": 0.0000017810460644893527, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with allowDots", + "hz": 423247.43042810756, + "rme": 0.39762832556297356, + "mean": 0.000002362684160866652, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify without encoding", + "hz": 305098.6459320467, + "rme": 0.45347341920540213, + "mean": 0.0000032776284435648584, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with query prefix", + "hz": 1521335.8323765784, + "rme": 0.23740575236633274, + "mean": 6.573170622280253e-7, + "sample": 99, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with custom delimiter", + "hz": 1133256.6174291337, + "rme": 1.250054485636678, + "mean": 8.824126721347235e-7, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify null values", + "hz": 1632457.729592944, + "rme": 1.211689815524943, + "mean": 6.12573288650697e-7, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with strictNullHandling", + "hz": 1242975.0468524443, + "rme": 0.5254459851625893, + "mean": 8.045213800005686e-7, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with skipNulls", + "hz": 1407734.5786021585, + "rme": 1.4627513633373666, + "mean": 7.103611825696378e-7, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify special characters", + "hz": 1187086.6120913841, + "rme": 0.4813794484571277, + "mean": 8.423985156720966e-7, + "sample": 93, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/index.js b/benchmark/index.js index 5bfc72c5..c88ce521 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -3,7 +3,7 @@ const runParseBenchmarks = require('./parse'); const runStringifyBenchmarks = require('./stringify'); -const chalk = require('chalk'); +const colors = require('colors'); const fs = require('fs'); const path = require('path'); @@ -11,11 +11,11 @@ async function main() { const args = process.argv.slice(2); const options = parseArgs(args); - console.log(chalk.blue.bold('πŸš€ QS Library Benchmarks')); - console.log(chalk.gray(`Node.js ${process.version} on ${process.platform}-${process.arch}\n`)); + console.log(colors.blue.bold('πŸš€ QS Library Benchmarks')); + console.log(colors.gray(`Node.js ${process.version} on ${process.platform}-${process.arch}\n`)); if (options.baseline) { - console.log(chalk.yellow(`πŸ“Š Comparing against baseline: ${options.baseline}\n`)); + console.log(colors.yellow(`πŸ“Š Comparing against baseline: ${options.baseline}\n`)); } try { @@ -34,14 +34,14 @@ async function main() { generateSummaryReport(); } - console.log(chalk.green('βœ… Benchmarks completed successfully!')); + console.log(colors.green('βœ… Benchmarks completed successfully!')); if (options.save) { saveBaseline(); } } catch (error) { - console.error(chalk.red('❌ Benchmark failed:'), error.message); + console.error(colors.red('❌ Benchmark failed:'), error.message); process.exit(1); } } @@ -81,7 +81,7 @@ function parseArgs(args) { process.exit(0); default: if (arg.startsWith('--')) { - console.error(chalk.red(`Unknown option: ${arg}`)); + console.error(colors.red(`Unknown option: ${arg}`)); printHelp(); process.exit(1); } @@ -93,12 +93,12 @@ function parseArgs(args) { function printHelp() { console.log(` -${chalk.blue.bold('QS Benchmark Suite')} +${colors.blue.bold('QS Benchmark Suite')} -${chalk.yellow('Usage:')} +${colors.yellow('Usage:')} node benchmark/index.js [options] -${chalk.yellow('Options:')} +${colors.yellow('Options:')} --parse Run only parse benchmarks --stringify Run only stringify benchmarks --baseline FILE Compare results against baseline file @@ -106,14 +106,14 @@ ${chalk.yellow('Options:')} --no-summary Skip summary report --help Show this help -${chalk.yellow('Examples:')} +${colors.yellow('Examples:')} node benchmark/index.js # Run all benchmarks node benchmark/index.js --parse # Run only parse benchmarks node benchmark/index.js --baseline baseline.json # Compare with baseline node benchmark/index.js --save-baseline # Save as new baseline STRESS_TEST=1 node benchmark/index.js # Include stress tests -${chalk.yellow('Environment Variables:')} +${colors.yellow('Environment Variables:')} STRESS_TEST=1 Include stress test scenarios `); } @@ -129,7 +129,7 @@ function generateSummaryReport() { if (files.length === 0) return; - console.log(chalk.blue('\nπŸ“ˆ Performance Trend (Last 5 Runs)\n')); + console.log(colors.blue('\nπŸ“ˆ Performance Trend (Last 5 Runs)\n')); const trends = {}; files.forEach(file => { @@ -164,7 +164,7 @@ function saveBaseline() { const baselinePath = path.join(__dirname, 'baseline.json'); fs.copyFileSync(path.join(resultsDir, latest), baselinePath); - console.log(chalk.green(`πŸ’Ύ Baseline saved to ${baselinePath}`)); + console.log(colors.green(`πŸ’Ύ Baseline saved to ${baselinePath}`)); } if (require.main === module) { diff --git a/benchmark/package.json b/benchmark/package.json index 42560010..643425e5 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -13,7 +13,7 @@ "dependencies": { "benchmark": "^2.1.4", "table": "^6.8.1", - "chalk": "^4.1.2" + "colors": "=1.4.0" } } diff --git a/benchmark/runner.js b/benchmark/runner.js index 54523979..69557330 100644 --- a/benchmark/runner.js +++ b/benchmark/runner.js @@ -2,7 +2,7 @@ const Benchmark = require('benchmark'); const { table } = require('table'); -const chalk = require('chalk'); +const colors = require('colors'); const fs = require('fs'); const path = require('path'); @@ -47,12 +47,12 @@ class BenchmarkRunner { this.suite .on('start', () => { if (this.options.verbose) { - console.log(chalk.blue('πŸš€ Starting benchmarks...')); + console.log(colors.blue('πŸš€ Starting benchmarks...')); } }) .on('cycle', (event) => { if (this.options.verbose) { - console.log(chalk.gray(String(event.target))); + console.log(colors.gray(String(event.target))); } }) .on('complete', () => { @@ -99,7 +99,7 @@ class BenchmarkRunner { } _printConsoleReport() { - console.log(chalk.green('\nπŸ“Š Benchmark Results\n')); + console.log(colors.green('\nπŸ“Š Benchmark Results\n')); const tableData = [ ['Test Name', 'Ops/sec', 'RME', 'Samples', 'Status'] @@ -125,13 +125,13 @@ class BenchmarkRunner { _compareWithBaseline(currentReport) { if (!fs.existsSync(this.options.baseline)) { - console.log(chalk.yellow('⚠️ Baseline file not found, skipping comparison')); + console.log(colors.yellow('⚠️ Baseline file not found, skipping comparison')); return; } try { const baseline = JSON.parse(fs.readFileSync(this.options.baseline, 'utf8')); - console.log(chalk.blue('\nπŸ“ˆ Performance Comparison\n')); + console.log(colors.blue('\nπŸ“ˆ Performance Comparison\n')); const comparisonData = [ ['Test Name', 'Current', 'Baseline', 'Change', 'Status'] @@ -158,7 +158,7 @@ class BenchmarkRunner { console.log(table(comparisonData)); } catch (error) { - console.log(chalk.red(`❌ Error comparing with baseline: ${error.message}`)); + console.log(colors.red(`❌ Error comparing with baseline: ${error.message}`)); } } } From 27d1c518f1d17238129b7d2cb2a1fe9baf3cd403 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Sat, 21 Jun 2025 01:46:22 +0300 Subject: [PATCH 04/14] fix(docs): update Node.js version range in benchmarking features --- benchmark/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/README.md b/benchmark/README.md index 5a64d275..ec42b388 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -5,7 +5,7 @@ This directory contains a comprehensive benchmarking infrastructure for the `qs` ## Features - πŸš€ **Comprehensive Test Coverage**: Parse and stringify operations across various scenarios -- πŸ“Š **Cross-Node Version Testing**: Automated testing on Node.js 16, 18, 20, and 22 +- πŸ“Š **Cross-Node Version Testing**: Automated testing from Node.js 0.8 through current versions - πŸ“ˆ **Regression Detection**: Compare performance against baselines - πŸ”„ **CI Integration**: Automated benchmarks on every PR and push - πŸ“‹ **Detailed Reporting**: Visual tables with performance metrics From 864df764aae5d7400689dc77db00a4c8494e8295 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Sat, 21 Jun 2025 17:04:41 +0300 Subject: [PATCH 05/14] feat(benchmark): add performance benchmarking workflow and comparison script --- .github/workflows/benchmark.yml | 81 +++++++++++++++++ benchmark/compare-benchmarks.js | 152 ++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 .github/workflows/benchmark.yml create mode 100644 benchmark/compare-benchmarks.js diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..bfd3f7d3 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,81 @@ +name: Performance Benchmark + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + benchmark: + name: Run Performance Benchmarks + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Get full history for comparison + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + # Install benchmark dependencies + - name: Install benchmark dependencies + run: cd benchmark && npm ci + + # For PRs: Run benchmarks on both the base branch and the PR branch + - name: Compare with base branch + if: ${{ github.event_name == 'pull_request' }} + run: | + # Save current branch name to go back later + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + + # Create directories for results + mkdir -p /tmp/qs-benchmark/base + mkdir -p /tmp/qs-benchmark/pr + + echo "==== Running benchmarks on base branch ====" + # Check out the base branch + git checkout origin/${{ github.base_ref }} + # Install dependencies (in case they changed) + npm ci + cd benchmark && npm ci && cd .. + # Run benchmarks and save results + node benchmark/index.js + cp benchmark/results/$(ls -t benchmark/results/benchmark-*.json | head -n 1) /tmp/qs-benchmark/base/results.json + + echo "==== Running benchmarks on PR branch ====" + # Go back to PR branch + git checkout $CURRENT_BRANCH + # Install dependencies again for PR + npm ci + cd benchmark && npm ci && cd .. + # Run benchmarks and save results + node benchmark/index.js + cp benchmark/results/$(ls -t benchmark/results/benchmark-*.json | head -n 1) /tmp/qs-benchmark/pr/results.json + + # Compare benchmark results using the comparison script + node benchmark/compare-benchmarks.js \ + /tmp/qs-benchmark/base/results.json \ + /tmp/qs-benchmark/pr/results.json \ + -10 # 10% slowdown threshold + + # For pushes to main: Run benchmarks and save as baseline + - name: Run benchmarks on main branch + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + run: | + # Run benchmarks + node benchmark/index.js + + # Save latest as baseline for future comparisons + echo "Saving latest benchmark as baseline..." + latest_result=$(ls -t benchmark/results/benchmark-*.json | head -n 1) + cp $latest_result benchmark/baseline.json + echo "Baseline saved to benchmark/baseline.json" diff --git a/benchmark/compare-benchmarks.js b/benchmark/compare-benchmarks.js new file mode 100644 index 00000000..9b83c7de --- /dev/null +++ b/benchmark/compare-benchmarks.js @@ -0,0 +1,152 @@ +'use strict'; + +/** + * Benchmark comparison script that compares two benchmark results and checks for performance regressions + * + * Usage: node compare-benchmarks.js [threshold] + * + * Arguments: + * - baseline-result.json: Path to the baseline benchmark result file + * - current-result.json: Path to the current benchmark result file + * - threshold: Optional performance regression threshold as a percentage (default: -10) + * + * Exit codes: + * - 0: No regressions detected + * - 1: Significant performance regression detected + * - 2: Error in script execution + */ + +const fs = require('fs'); +const path = require('path'); +const colors = require('colors'); + +// Check arguments +if (process.argv.length < 4) { + console.error('Usage: node compare-benchmarks.js [threshold]'); + process.exit(2); +} + +// Parse arguments +const baselinePath = process.argv[2]; +const currentPath = process.argv[3]; +const threshold = parseFloat(process.argv[4] || '-10'); // Default: 10% slowdown threshold + +// Validate files +if (!fs.existsSync(baselinePath)) { + console.error(`Error: Baseline file '${baselinePath}' does not exist`); + process.exit(2); +} + +if (!fs.existsSync(currentPath)) { + console.error(`Error: Current file '${currentPath}' does not exist`); + process.exit(2); +} + +// Read and parse results +let baseline, current; + +try { + baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8')); +} catch (error) { + console.error(`Error parsing baseline file: ${error.message}`); + process.exit(2); +} + +try { + current = JSON.parse(fs.readFileSync(currentPath, 'utf8')); +} catch (error) { + console.error(`Error parsing current file: ${error.message}`); + process.exit(2); +} + +// Compare results +let hasSevereSlowDown = false; +const failingTests = []; +const allTests = []; + +// Compare each benchmark +current.results.forEach(result => { + const baseResult = baseline.results.find(b => b.name === result.name); + if (!baseResult) return; + + const change = ((result.hz - baseResult.hz) / baseResult.hz * 100); + + // Add to all tests for summary report + allTests.push({ + name: result.name, + change: change.toFixed(2), + current: result.hz.toLocaleString(), + baseline: baseResult.hz.toLocaleString() + }); + + if (change < threshold) { + failingTests.push({ + name: result.name, + change: change.toFixed(2), + current: result.hz.toLocaleString(), + baseline: baseResult.hz.toLocaleString() + }); + hasSevereSlowDown = true; + } +}); + +// Calculate statistics +const changes = allTests.map(test => parseFloat(test.change)); +const avgChange = changes.reduce((sum, val) => sum + val, 0) / changes.length; +const minChange = Math.min(...changes); +const maxChange = Math.max(...changes); + +// Print summary +console.log(colors.blue('\nπŸ“Š Performance Comparison Summary:')); +console.log(colors.gray(`\nNode.js: ${process.version} | OS: ${process.platform}-${process.arch}`)); +console.log(colors.gray(`Baseline: ${path.basename(baselinePath)} | Current: ${path.basename(currentPath)}`)); +console.log(colors.gray(`Threshold: ${threshold}% | Average Change: ${avgChange.toFixed(2)}% | Range: ${minChange.toFixed(2)}% to ${maxChange.toFixed(2)}%`)); + +// Sort tests by change (worst first) +allTests.sort((a, b) => parseFloat(a.change) - parseFloat(b.change)); + +console.log('\nDETAILED RESULTS:'); +allTests.forEach(test => { + const changeNum = parseFloat(test.change); + + // Determine indicator icon based on change magnitude + let indicator; + if (changeNum < -5) { + indicator = colors.red('πŸ”»'); + } else if (changeNum < 0) { + indicator = colors.yellow('β–Ό'); + } else if (changeNum > 5) { + indicator = colors.green('πŸ”Ό'); + } else if (changeNum > 0) { + indicator = colors.green('β–²'); + } else { + indicator = colors.gray('βž–'); + } + + const changeText = changeNum >= 0 ? `+${test.change}%` : `${test.change}%`; + + // Determine color based on change magnitude + let changeColor; + if (changeNum < threshold) { + changeColor = colors.red; + } else if (changeNum < 0) { + changeColor = colors.yellow; + } else if (changeNum > 0) { + changeColor = colors.green; + } else { + changeColor = colors.gray; + } + + console.log(`${indicator} ${test.name}: ${changeColor(changeText)} (${test.current} vs ${test.baseline} ops/sec)`); +}); + +if (hasSevereSlowDown) { + console.log(colors.red(`\n❌ Performance regression detected! The following tests exceed the threshold of ${Math.abs(threshold)}% slowdown:`)); + failingTests.forEach(test => { + console.log(colors.red(`- ${test.name}: ${test.change}% (${test.current} vs ${test.baseline} ops/sec)`)); + }); + process.exit(1); +} else { + console.log(colors.green(`\nβœ… No significant performance regressions detected.`)); + process.exit(0); +} From 67ac36c094396dfa8d8bbca88a4fc7c5b608676d Mon Sep 17 00:00:00 2001 From: Mohamed Date: Sat, 21 Jun 2025 23:14:02 +0300 Subject: [PATCH 06/14] fix(workflow): update Node.js setup and dependency installation in benchmark workflow --- .github/workflows/benchmark.yml | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index bfd3f7d3..1bf2927e 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,18 +17,12 @@ jobs: with: fetch-depth: 0 # Get full history for comparison - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - name: Install dependencies - run: npm ci + uses: ljharb/actions/node/install@main # Install benchmark dependencies - name: Install benchmark dependencies - run: cd benchmark && npm ci + run: cd benchmark && npm install # For PRs: Run benchmarks on both the base branch and the PR branch - name: Compare with base branch @@ -45,8 +39,8 @@ jobs: # Check out the base branch git checkout origin/${{ github.base_ref }} # Install dependencies (in case they changed) - npm ci - cd benchmark && npm ci && cd .. + npm install + cd benchmark && npm install && cd .. # Run benchmarks and save results node benchmark/index.js cp benchmark/results/$(ls -t benchmark/results/benchmark-*.json | head -n 1) /tmp/qs-benchmark/base/results.json @@ -55,8 +49,8 @@ jobs: # Go back to PR branch git checkout $CURRENT_BRANCH # Install dependencies again for PR - npm ci - cd benchmark && npm ci && cd .. + npm install + cd benchmark && npm install && cd .. # Run benchmarks and save results node benchmark/index.js cp benchmark/results/$(ls -t benchmark/results/benchmark-*.json | head -n 1) /tmp/qs-benchmark/pr/results.json @@ -73,9 +67,9 @@ jobs: run: | # Run benchmarks node benchmark/index.js - + # Save latest as baseline for future comparisons echo "Saving latest benchmark as baseline..." latest_result=$(ls -t benchmark/results/benchmark-*.json | head -n 1) - cp $latest_result benchmark/baseline.json + cp $latest_result benchmark/baseline.json echo "Baseline saved to benchmark/baseline.json" From 9e54f203746814fb0bb44d79fb5de562a9f0d9af Mon Sep 17 00:00:00 2001 From: Mohamed Date: Tue, 24 Jun 2025 18:43:07 +0300 Subject: [PATCH 07/14] feat: translate es6 to es5 & enhance benchmarking workflows --- .eslintrc | 1 + .github/workflows/benchmark.yml | 63 ++-- benchmark/baseline.json | 169 +++++++++ .../results/benchmark-1750454342309.json | 169 --------- .../results/benchmark-1750454442152.json | 160 --------- .../results/benchmark-1750454596215.json | 169 --------- .../results/benchmark-1750458974767.json | 169 --------- .../results/benchmark-1750778180061.json | 169 +++++++++ ...4889.json => benchmark-1750778282626.json} | 134 +++---- .../results/benchmark-1750778914140.json | 169 +++++++++ .../results/benchmark-1750779015703.json | 160 +++++++++ benchmark/compare-benchmarks.js | 152 -------- benchmark/fixtures.js | 180 ++++++++-- benchmark/index.js | 239 ++++++++----- benchmark/package.json | 8 - benchmark/parse.js | 66 ++-- benchmark/polyfills.js | 187 ++++++++++ .../results/benchmark-1750775607814.json | 169 +++++++++ benchmark/runner.js | 337 ++++++++++-------- benchmark/stringify.js | 60 ++-- package.json | 6 + 21 files changed, 1691 insertions(+), 1245 deletions(-) create mode 100644 benchmark/baseline.json delete mode 100644 benchmark/benchmark/results/benchmark-1750454342309.json delete mode 100644 benchmark/benchmark/results/benchmark-1750454442152.json delete mode 100644 benchmark/benchmark/results/benchmark-1750454596215.json delete mode 100644 benchmark/benchmark/results/benchmark-1750458974767.json create mode 100644 benchmark/benchmark/results/benchmark-1750778180061.json rename benchmark/benchmark/results/{benchmark-1750459074889.json => benchmark-1750778282626.json} (50%) create mode 100644 benchmark/benchmark/results/benchmark-1750778914140.json create mode 100644 benchmark/benchmark/results/benchmark-1750779015703.json delete mode 100644 benchmark/compare-benchmarks.js create mode 100644 benchmark/polyfills.js create mode 100644 benchmark/results/benchmark-1750775607814.json diff --git a/.eslintrc b/.eslintrc index a89f60e6..955c85be 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,6 +5,7 @@ "ignorePatterns": [ "dist/", + "benchmark/", ], "rules": { diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 1bf2927e..9cdb7bc7 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -11,65 +11,42 @@ jobs: name: Run Performance Benchmarks runs-on: ubuntu-latest + strategy: + matrix: + node-version: [0.8, 0.9, 0.10, 0.11, 0.12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + fail-fast: false + steps: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 # Get full history for comparison + fetch-depth: 0 - name: Install dependencies uses: ljharb/actions/node/install@main + with: + node-version: ${{ matrix.node-version }} - # Install benchmark dependencies - name: Install benchmark dependencies run: cd benchmark && npm install - # For PRs: Run benchmarks on both the base branch and the PR branch - - name: Compare with base branch + # For PRs: Run benchmarks and compare against committed baseline + - name: Run benchmarks and compare against baseline if: ${{ github.event_name == 'pull_request' }} run: | - # Save current branch name to go back later - CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) - - # Create directories for results - mkdir -p /tmp/qs-benchmark/base - mkdir -p /tmp/qs-benchmark/pr - - echo "==== Running benchmarks on base branch ====" - # Check out the base branch - git checkout origin/${{ github.base_ref }} - # Install dependencies (in case they changed) - npm install - cd benchmark && npm install && cd .. - # Run benchmarks and save results - node benchmark/index.js - cp benchmark/results/$(ls -t benchmark/results/benchmark-*.json | head -n 1) /tmp/qs-benchmark/base/results.json - - echo "==== Running benchmarks on PR branch ====" - # Go back to PR branch - git checkout $CURRENT_BRANCH - # Install dependencies again for PR - npm install - cd benchmark && npm install && cd .. - # Run benchmarks and save results - node benchmark/index.js - cp benchmark/results/$(ls -t benchmark/results/benchmark-*.json | head -n 1) /tmp/qs-benchmark/pr/results.json + echo "==== Running benchmarks and comparing against baseline ====" + # Run benchmarks using the compare script that uses the committed baseline + npm run benchmark:compare - # Compare benchmark results using the comparison script - node benchmark/compare-benchmarks.js \ - /tmp/qs-benchmark/base/results.json \ - /tmp/qs-benchmark/pr/results.json \ - -10 # 10% slowdown threshold + # Print the Node.js version for debugging + echo "Benchmark completed on Node.js version: $(node --version)" - # For pushes to main: Run benchmarks and save as baseline + # For pushes to main: Run benchmarks against the committed baseline - name: Run benchmarks on main branch if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} run: | - # Run benchmarks - node benchmark/index.js + # Run benchmarks against the committed baseline + npm run benchmark:compare - # Save latest as baseline for future comparisons - echo "Saving latest benchmark as baseline..." - latest_result=$(ls -t benchmark/results/benchmark-*.json | head -n 1) - cp $latest_result benchmark/baseline.json - echo "Baseline saved to benchmark/baseline.json" + # Print the Node.js version for debugging + echo "Benchmark completed on Node.js version: $(node --version)" diff --git a/benchmark/baseline.json b/benchmark/baseline.json new file mode 100644 index 00000000..9bda7aea --- /dev/null +++ b/benchmark/baseline.json @@ -0,0 +1,169 @@ +{ + "timestamp": "2025-06-24T14:33:27.757Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Parse simple tiny query", + "hz": 339368.57646296266, + "rme": 1.5877642145574582, + "mean": 0.000002946648774681518, + "sample": 93, + "fastest": true, + "slowest": false + }, + { + "name": "Parse simple small query", + "hz": 166303.78950936385, + "rme": 1.0841983813030203, + "mean": 0.000006013092082569137, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple medium query", + "hz": 38462.85790917391, + "rme": 1.4113066897602626, + "mean": 0.000025999108083996187, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple large query", + "hz": 8062.515205586793, + "rme": 1.057628921792131, + "mean": 0.00012403077383433221, + "sample": 93, + "fastest": false, + "slowest": true + }, + { + "name": "Parse shallow nested", + "hz": 169808.09843727067, + "rme": 0.8811370835416009, + "mean": 0.000005889000637795923, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse medium nested", + "hz": 132794.2803924927, + "rme": 0.6406034191302279, + "mean": 0.0000075304448131678215, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse deep nested", + "hz": 72472.98755695937, + "rme": 0.6823000589684686, + "mean": 0.00001379824447300535, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple array", + "hz": 193669.5397470639, + "rme": 0.5945380745133809, + "mean": 0.000005163434587111731, + "sample": 99, + "fastest": false, + "slowest": false + }, + { + "name": "Parse indexed array", + "hz": 111618.07377058547, + "rme": 1.389286676498194, + "mean": 0.00000895912253471918, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse mixed array/object", + "hz": 79964.95688603172, + "rme": 1.4153317502720602, + "mean": 0.000012505477886084874, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse e-commerce query", + "hz": 55869.88938147373, + "rme": 0.8232662184894164, + "mean": 0.00001789872883356015, + "sample": 86, + "fastest": false, + "slowest": false + }, + { + "name": "Parse form data", + "hz": 64758.34859642685, + "rme": 1.0831317727449514, + "mean": 0.000015442024413438744, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse API query", + "hz": 50796.577670369334, + "rme": 1.792982399893066, + "mean": 0.00001968636561481031, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Parse empty values", + "hz": 249045.98363261085, + "rme": 0.9984244240955832, + "mean": 0.000004015322734436007, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse special characters", + "hz": 326350.9418241071, + "rme": 0.8537521124373892, + "mean": 0.000003064186039760132, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with allowDots", + "hz": 188545.89300983382, + "rme": 0.9810731436902679, + "mean": 0.0000053037485146804226, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with comma arrays", + "hz": 324909.63376456907, + "rme": 0.6122238240837989, + "mean": 0.0000030777788531952375, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with depth limit", + "hz": 77198.2278999816, + "rme": 1.1472932485238967, + "mean": 0.000012953665222673309, + "sample": 98, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750454342309.json b/benchmark/benchmark/results/benchmark-1750454342309.json deleted file mode 100644 index 7b832316..00000000 --- a/benchmark/benchmark/results/benchmark-1750454342309.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "timestamp": "2025-06-20T21:19:02.296Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Parse simple tiny query", - "hz": 697833.6386936898, - "rme": 1.1681832751356365, - "mean": 0.000001433006299140223, - "sample": 91, - "fastest": true, - "slowest": false - }, - { - "name": "Parse simple small query", - "hz": 333344.8868012807, - "rme": 0.7665510509001657, - "mean": 0.0000029998960223923796, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple medium query", - "hz": 81681.29623992334, - "rme": 0.6887321102570698, - "mean": 0.000012242704830034644, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple large query", - "hz": 16183.406763108791, - "rme": 0.8353154746767609, - "mean": 0.0000617916866725225, - "sample": 95, - "fastest": false, - "slowest": true - }, - { - "name": "Parse shallow nested", - "hz": 331721.95409273804, - "rme": 0.7740676253490404, - "mean": 0.0000030145728603794324, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Parse medium nested", - "hz": 262336.0057169703, - "rme": 0.4314241603618106, - "mean": 0.0000038119052596953937, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Parse deep nested", - "hz": 144619.34728908024, - "rme": 0.44536020425504225, - "mean": 0.000006914704143983555, - "sample": 99, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple array", - "hz": 374633.2818277549, - "rme": 0.7104402811137674, - "mean": 0.0000026692769930135833, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse indexed array", - "hz": 220048.48148864487, - "rme": 0.49671738413420585, - "mean": 0.000004544453082497653, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse mixed array/object", - "hz": 157740.04879441197, - "rme": 0.5853969792354998, - "mean": 0.000006339544127460836, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse e-commerce query", - "hz": 112051.98143502424, - "rme": 0.5159663283577665, - "mean": 0.000008924429422784207, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse form data", - "hz": 131241.05545123873, - "rme": 0.2051087777285156, - "mean": 0.000007619566884476479, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse API query", - "hz": 102799.06016443495, - "rme": 0.3642870811382813, - "mean": 0.000009727715393510638, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse empty values", - "hz": 508054.16454990895, - "rme": 0.23030272252997103, - "mean": 0.0000019682940713337357, - "sample": 100, - "fastest": false, - "slowest": false - }, - { - "name": "Parse special characters", - "hz": 666303.7335275853, - "rme": 0.170597791898354, - "mean": 0.0000015008170443616455, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with allowDots", - "hz": 387799.56779563095, - "rme": 0.24773325732867316, - "mean": 0.0000025786516619507854, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with comma arrays", - "hz": 639742.9214602712, - "rme": 0.6495100337107264, - "mean": 0.0000015631278853659048, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with depth limit", - "hz": 153594.02605669567, - "rme": 0.5349662303802443, - "mean": 0.0000065106698852393725, - "sample": 94, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750454442152.json b/benchmark/benchmark/results/benchmark-1750454442152.json deleted file mode 100644 index d56fc7bb..00000000 --- a/benchmark/benchmark/results/benchmark-1750454442152.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "timestamp": "2025-06-20T21:20:42.149Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Stringify simple object", - "hz": 2037374.6865790153, - "rme": 0.38175901075425256, - "mean": 4.908277336453582e-7, - "sample": 96, - "fastest": true, - "slowest": false - }, - { - "name": "Stringify nested object", - "hz": 408556.066063155, - "rme": 0.3368748760920129, - "mean": 0.000002447644480318202, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify arrays", - "hz": 226212.74356404794, - "rme": 0.8662747505196786, - "mean": 0.0000044206174428757084, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify mixed object", - "hz": 117138.94402626439, - "rme": 0.749993217517359, - "mean": 0.00000853687053705883, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify large object", - "hz": 28825.879879177846, - "rme": 0.38975236367677246, - "mean": 0.00003469104860602511, - "sample": 96, - "fastest": false, - "slowest": true - }, - { - "name": "Stringify array indices format", - "hz": 219437.76757235164, - "rme": 0.7290565006242969, - "mean": 0.000004557100680812779, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify array brackets format", - "hz": 226129.31509867168, - "rme": 0.3911649465251547, - "mean": 0.0000044222483916499255, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify array repeat format", - "hz": 250559.12014171766, - "rme": 0.48659264259497126, - "mean": 0.0000039910740404675525, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify array comma format", - "hz": 572028.3255381505, - "rme": 0.2674909038397102, - "mean": 0.0000017481651788121228, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with allowDots", - "hz": 423312.84029828827, - "rme": 0.47859322788251846, - "mean": 0.000002362319081309577, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify without encoding", - "hz": 314276.9997364952, - "rme": 0.5185917828975549, - "mean": 0.000003181906410072794, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with query prefix", - "hz": 1540447.7269144836, - "rme": 0.44489478007002803, - "mean": 6.491619173621683e-7, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with custom delimiter", - "hz": 1198322.2475114788, - "rme": 0.3984677658832, - "mean": 8.345000704749253e-7, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify null values", - "hz": 1798516.1840537034, - "rme": 0.38608776984646054, - "mean": 5.560139012739294e-7, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with strictNullHandling", - "hz": 1319198.5442423075, - "rme": 0.32916194615612765, - "mean": 7.58036009336531e-7, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with skipNulls", - "hz": 1532309.748620865, - "rme": 0.2201108754463058, - "mean": 6.52609565983664e-7, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify special characters", - "hz": 1232357.2098995054, - "rme": 0.36282680440289805, - "mean": 8.114530364792093e-7, - "sample": 96, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750454596215.json b/benchmark/benchmark/results/benchmark-1750454596215.json deleted file mode 100644 index 06b7f871..00000000 --- a/benchmark/benchmark/results/benchmark-1750454596215.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "timestamp": "2025-06-20T21:23:16.201Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Parse simple tiny query", - "hz": 723258.9943741269, - "rme": 0.35483449296383623, - "mean": 0.0000013826305760156515, - "sample": 95, - "fastest": true, - "slowest": false - }, - { - "name": "Parse simple small query", - "hz": 355150.7402551628, - "rme": 0.1424851978448738, - "mean": 0.0000028157058022222806, - "sample": 100, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple medium query", - "hz": 84104.35454009412, - "rme": 0.39088580594264205, - "mean": 0.000011889990779529511, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple large query", - "hz": 16509.73850456047, - "rme": 0.276763071980723, - "mean": 0.00006057031125742971, - "sample": 96, - "fastest": false, - "slowest": true - }, - { - "name": "Parse shallow nested", - "hz": 350204.28068824776, - "rme": 0.4422855453635856, - "mean": 0.0000028554762324284697, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse medium nested", - "hz": 259764.0327765841, - "rme": 0.11239456732731663, - "mean": 0.000003849647656417748, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse deep nested", - "hz": 145215.76333611264, - "rme": 0.10667275458392099, - "mean": 0.00000688630474424065, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple array", - "hz": 393278.25699955504, - "rme": 0.10987173378822206, - "mean": 0.00000254272892589923, - "sample": 100, - "fastest": false, - "slowest": false - }, - { - "name": "Parse indexed array", - "hz": 234055.33468120755, - "rme": 0.4110444970997853, - "mean": 0.000004272493944058309, - "sample": 90, - "fastest": false, - "slowest": false - }, - { - "name": "Parse mixed array/object", - "hz": 164416.117203606, - "rme": 0.32599376184516793, - "mean": 0.000006082128790096911, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse e-commerce query", - "hz": 118759.72830911238, - "rme": 0.47318027941807034, - "mean": 0.000008420362813538623, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse form data", - "hz": 134391.6156726525, - "rme": 0.3497367447128347, - "mean": 0.000007440940381547115, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Parse API query", - "hz": 105333.06194387515, - "rme": 0.12438374842192938, - "mean": 0.000009493695346413002, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse empty values", - "hz": 509933.3278528553, - "rme": 0.18355617829640952, - "mean": 0.000001961040679985829, - "sample": 89, - "fastest": false, - "slowest": false - }, - { - "name": "Parse special characters", - "hz": 677648.6347977747, - "rme": 0.4228916585233394, - "mean": 0.00000147569101249414, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with allowDots", - "hz": 382126.8096919969, - "rme": 0.10508550735949418, - "mean": 0.0000026169323236075042, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with comma arrays", - "hz": 664503.237195686, - "rme": 0.1220931753822807, - "mean": 0.0000015048835641797112, - "sample": 101, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with depth limit", - "hz": 156745.76615928378, - "rme": 0.0899787948667524, - "mean": 0.000006379757645152648, - "sample": 101, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750458974767.json b/benchmark/benchmark/results/benchmark-1750458974767.json deleted file mode 100644 index dac61366..00000000 --- a/benchmark/benchmark/results/benchmark-1750458974767.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "timestamp": "2025-06-20T22:36:14.754Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Parse simple tiny query", - "hz": 741706.0976391918, - "rme": 0.291338734691978, - "mean": 0.0000013482429269261004, - "sample": 99, - "fastest": true, - "slowest": false - }, - { - "name": "Parse simple small query", - "hz": 353993.26933922776, - "rme": 0.39354178718805893, - "mean": 0.0000028249124675918946, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple medium query", - "hz": 82378.43106603576, - "rme": 0.30148925498931284, - "mean": 0.000012139099847609203, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple large query", - "hz": 16668.393821329242, - "rme": 0.4482299400778523, - "mean": 0.000059993782887489625, - "sample": 89, - "fastest": false, - "slowest": true - }, - { - "name": "Parse shallow nested", - "hz": 346343.1409931819, - "rme": 0.1502699708357385, - "mean": 0.0000028873099583620335, - "sample": 99, - "fastest": false, - "slowest": false - }, - { - "name": "Parse medium nested", - "hz": 269390.6093815785, - "rme": 0.13230828389767865, - "mean": 0.0000037120818810114847, - "sample": 100, - "fastest": false, - "slowest": false - }, - { - "name": "Parse deep nested", - "hz": 150858.36475971554, - "rme": 0.3992477649667113, - "mean": 0.000006628734187811076, - "sample": 99, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple array", - "hz": 394986.18072817277, - "rme": 0.27325821232522357, - "mean": 0.000002531734143600822, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse indexed array", - "hz": 227041.01456683056, - "rme": 0.5890672990218405, - "mean": 0.000004404490536249103, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse mixed array/object", - "hz": 164424.52635209722, - "rme": 0.3700866902750369, - "mean": 0.000006081817732341274, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Parse e-commerce query", - "hz": 116458.2283425666, - "rme": 0.4105686178223879, - "mean": 0.000008586769816371063, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse form data", - "hz": 130247.8388317256, - "rme": 0.18328636288465622, - "mean": 0.00000767767057764356, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse API query", - "hz": 105523.63100473923, - "rme": 0.23687452866920514, - "mean": 0.000009476550327907958, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse empty values", - "hz": 517989.8737175659, - "rme": 0.4010984920826836, - "mean": 0.000001930539670250871, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse special characters", - "hz": 660999.1463362356, - "rme": 0.25922009614695707, - "mean": 0.0000015128612579044424, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with allowDots", - "hz": 383837.1475119348, - "rme": 0.18513550518740124, - "mean": 0.000002605271549359111, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with comma arrays", - "hz": 657057.0874023354, - "rme": 0.14373804238904503, - "mean": 0.0000015219377724901864, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with depth limit", - "hz": 156256.49658681112, - "rme": 0.2981065395867695, - "mean": 0.000006399733910867712, - "sample": 95, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750778180061.json b/benchmark/benchmark/results/benchmark-1750778180061.json new file mode 100644 index 00000000..a4c88d59 --- /dev/null +++ b/benchmark/benchmark/results/benchmark-1750778180061.json @@ -0,0 +1,169 @@ +{ + "timestamp": "2025-06-24T15:16:20.033Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Parse simple tiny query", + "hz": 330180.9352747842, + "rme": 1.2821140616138376, + "mean": 0.000003028642459831234, + "sample": 91, + "fastest": true, + "slowest": false + }, + { + "name": "Parse simple small query", + "hz": 164671.8191954346, + "rme": 1.4978267342248865, + "mean": 0.000006072684475618669, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple medium query", + "hz": 40605.79161576655, + "rme": 1.5709717945711017, + "mean": 0.000024627028810631943, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple large query", + "hz": 8067.461185172645, + "rme": 0.8158292309924179, + "mean": 0.00012395473334757666, + "sample": 97, + "fastest": false, + "slowest": true + }, + { + "name": "Parse shallow nested", + "hz": 161603.80309589015, + "rme": 1.7450685612920416, + "mean": 0.000006187973184063214, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Parse medium nested", + "hz": 129486.0346918172, + "rme": 1.2327906662777717, + "mean": 0.000007722840554813858, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Parse deep nested", + "hz": 70654.74651251831, + "rme": 1.0509502882828092, + "mean": 0.000014153330800257338, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple array", + "hz": 179612.67386512936, + "rme": 1.8471490503828525, + "mean": 0.0000055675358452204615, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Parse indexed array", + "hz": 108637.71584793365, + "rme": 1.371249631410273, + "mean": 0.000009204906345782863, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Parse mixed array/object", + "hz": 76987.39594733487, + "rme": 1.7231392056956119, + "mean": 0.000012989139166157467, + "sample": 90, + "fastest": false, + "slowest": false + }, + { + "name": "Parse e-commerce query", + "hz": 52859.8666736335, + "rme": 1.6351653566587596, + "mean": 0.000018917944045795332, + "sample": 88, + "fastest": false, + "slowest": false + }, + { + "name": "Parse form data", + "hz": 62734.963135179736, + "rme": 1.3567458992443153, + "mean": 0.00001594007472109651, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Parse API query", + "hz": 50810.20825889967, + "rme": 1.0367452114983366, + "mean": 0.00001968108445658348, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Parse empty values", + "hz": 247938.09095715787, + "rme": 1.3576053106540886, + "mean": 0.000004033264901490242, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Parse special characters", + "hz": 312613.0302993482, + "rme": 2.004716867955466, + "mean": 0.0000031988429882223147, + "sample": 90, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with allowDots", + "hz": 179223.81655432936, + "rme": 1.4226944866292626, + "mean": 0.0000055796155847225976, + "sample": 86, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with comma arrays", + "hz": 318063.30084301333, + "rme": 1.105069678634199, + "mean": 0.0000031440282401318926, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with depth limit", + "hz": 75213.73776598606, + "rme": 1.1452004940278429, + "mean": 0.000013295443488147326, + "sample": 93, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750459074889.json b/benchmark/benchmark/results/benchmark-1750778282626.json similarity index 50% rename from benchmark/benchmark/results/benchmark-1750459074889.json rename to benchmark/benchmark/results/benchmark-1750778282626.json index 0293c910..27baf0ac 100644 --- a/benchmark/benchmark/results/benchmark-1750459074889.json +++ b/benchmark/benchmark/results/benchmark-1750778282626.json @@ -1,158 +1,158 @@ { - "timestamp": "2025-06-20T22:37:54.886Z", + "timestamp": "2025-06-24T15:18:02.620Z", "nodeVersion": "v20.10.0", "platform": "linux-x64", "results": [ { "name": "Stringify simple object", - "hz": 2024182.530416778, - "rme": 0.46962465183362023, - "mean": 4.940265934387353e-7, - "sample": 92, + "hz": 976109.2038787514, + "rme": 1.4366934996007836, + "mean": 0.0000010244755361657426, + "sample": 89, "fastest": true, "slowest": false }, { "name": "Stringify nested object", - "hz": 408904.35155663424, - "rme": 0.16901447303840778, - "mean": 0.000002445559691876983, + "hz": 194985.4588845839, + "rme": 1.4795812503560726, + "mean": 0.000005128587566070358, "sample": 91, "fastest": false, "slowest": false }, { "name": "Stringify arrays", - "hz": 242431.34212288828, - "rme": 0.329612211801757, - "mean": 0.0000041248791977280756, + "hz": 114848.53681820467, + "rme": 1.0304037149913832, + "mean": 0.00000870712007052309, "sample": 94, "fastest": false, "slowest": false }, { "name": "Stringify mixed object", - "hz": 123572.12030502668, - "rme": 0.4262874350371448, - "mean": 0.000008092440248913669, - "sample": 90, + "hz": 59549.669724762505, + "rme": 0.7216230963328606, + "mean": 0.000016792704386472365, + "sample": 91, "fastest": false, "slowest": false }, { "name": "Stringify large object", - "hz": 28745.95525484991, - "rme": 0.2561594313882339, - "mean": 0.00003478750283768301, - "sample": 96, + "hz": 16029.81031807559, + "rme": 0.8349993480545844, + "mean": 0.00006238376999834967, + "sample": 94, "fastest": false, "slowest": true }, { "name": "Stringify array indices format", - "hz": 220190.31018409316, - "rme": 0.46934638208201307, - "mean": 0.000004541525915304521, - "sample": 98, + "hz": 113602.64926783308, + "rme": 0.373741414185561, + "mean": 0.000008802611615529928, + "sample": 94, "fastest": false, "slowest": false }, { "name": "Stringify array brackets format", - "hz": 233458.89117637026, - "rme": 0.4558774545584227, - "mean": 0.00000428340936154166, - "sample": 95, + "hz": 117192.3740575468, + "rme": 0.6084953120963555, + "mean": 0.000008532978430055137, + "sample": 96, "fastest": false, "slowest": false }, { "name": "Stringify array repeat format", - "hz": 256700.1583741599, - "rme": 0.40762738534430965, - "mean": 0.000003895595570854399, - "sample": 98, + "hz": 127845.939039929, + "rme": 0.874105890944508, + "mean": 0.000007821914466033049, + "sample": 94, "fastest": false, "slowest": false }, { "name": "Stringify array comma format", - "hz": 561467.7912818117, - "rme": 0.15926460902620682, - "mean": 0.0000017810460644893527, - "sample": 97, + "hz": 295392.78285483853, + "rme": 0.9042362757667536, + "mean": 0.0000033853230615028887, + "sample": 93, "fastest": false, "slowest": false }, { "name": "Stringify with allowDots", - "hz": 423247.43042810756, - "rme": 0.39762832556297356, - "mean": 0.000002362684160866652, - "sample": 96, + "hz": 205293.44623108386, + "rme": 1.4258567729056548, + "mean": 0.000004871076102811256, + "sample": 92, "fastest": false, "slowest": false }, { "name": "Stringify without encoding", - "hz": 305098.6459320467, - "rme": 0.45347341920540213, - "mean": 0.0000032776284435648584, - "sample": 94, + "hz": 154230.07211793793, + "rme": 1.735820829004054, + "mean": 0.000006483819830125682, + "sample": 91, "fastest": false, "slowest": false }, { "name": "Stringify with query prefix", - "hz": 1521335.8323765784, - "rme": 0.23740575236633274, - "mean": 6.573170622280253e-7, - "sample": 99, + "hz": 768363.2047420949, + "rme": 0.9492095133249806, + "mean": 0.000001301467839464873, + "sample": 96, "fastest": false, "slowest": false }, { "name": "Stringify with custom delimiter", - "hz": 1133256.6174291337, - "rme": 1.250054485636678, - "mean": 8.824126721347235e-7, - "sample": 94, + "hz": 597488.6919134173, + "rme": 0.9812478112745247, + "mean": 0.0000016736718427215205, + "sample": 93, "fastest": false, "slowest": false }, { "name": "Stringify null values", - "hz": 1632457.729592944, - "rme": 1.211689815524943, - "mean": 6.12573288650697e-7, - "sample": 96, + "hz": 930733.0625960926, + "rme": 0.8599816731412604, + "mean": 0.0000010744219155712609, + "sample": 92, "fastest": false, "slowest": false }, { "name": "Stringify with strictNullHandling", - "hz": 1242975.0468524443, - "rme": 0.5254459851625893, - "mean": 8.045213800005686e-7, - "sample": 96, + "hz": 646752.215555868, + "rme": 0.699192956942562, + "mean": 0.0000015461872042301766, + "sample": 97, "fastest": false, "slowest": false }, { "name": "Stringify with skipNulls", - "hz": 1407734.5786021585, - "rme": 1.4627513633373666, - "mean": 7.103611825696378e-7, - "sample": 96, + "hz": 728271.5695564854, + "rme": 1.2819874334336552, + "mean": 0.0000013731141538437319, + "sample": 97, "fastest": false, "slowest": false }, { "name": "Stringify special characters", - "hz": 1187086.6120913841, - "rme": 0.4813794484571277, - "mean": 8.423985156720966e-7, - "sample": 93, + "hz": 606082.0112197315, + "rme": 1.437039782903162, + "mean": 0.0000016499417265124139, + "sample": 88, "fastest": false, "slowest": false } diff --git a/benchmark/benchmark/results/benchmark-1750778914140.json b/benchmark/benchmark/results/benchmark-1750778914140.json new file mode 100644 index 00000000..90b552b8 --- /dev/null +++ b/benchmark/benchmark/results/benchmark-1750778914140.json @@ -0,0 +1,169 @@ +{ + "timestamp": "2025-06-24T15:28:34.112Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Parse simple tiny query", + "hz": 313154.9237376186, + "rme": 1.6903268709844506, + "mean": 0.0000031933076065502473, + "sample": 89, + "fastest": true, + "slowest": false + }, + { + "name": "Parse simple small query", + "hz": 156105.16490604475, + "rme": 1.4911410003488408, + "mean": 0.000006405937949598731, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple medium query", + "hz": 38279.531183714214, + "rme": 1.3721360582304987, + "mean": 0.000026123621922136908, + "sample": 88, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple large query", + "hz": 7721.711765875682, + "rme": 1.4168845304510336, + "mean": 0.00012950496344855405, + "sample": 91, + "fastest": false, + "slowest": true + }, + { + "name": "Parse shallow nested", + "hz": 159240.97774527723, + "rme": 2.077739988722212, + "mean": 0.000006279790630271096, + "sample": 90, + "fastest": false, + "slowest": false + }, + { + "name": "Parse medium nested", + "hz": 125325.57762735152, + "rme": 1.4522640415354382, + "mean": 0.000007979217163262898, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Parse deep nested", + "hz": 67652.28462050331, + "rme": 1.283961566034765, + "mean": 0.000014781466813863238, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple array", + "hz": 184863.47901255573, + "rme": 1.1110156469307348, + "mean": 0.000005409397277068885, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse indexed array", + "hz": 109029.5236057738, + "rme": 1.346927383395607, + "mean": 0.000009171827656660913, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse mixed array/object", + "hz": 77122.65083054845, + "rme": 1.595987778716788, + "mean": 0.00001296635928914282, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Parse e-commerce query", + "hz": 54834.53920410963, + "rme": 1.3819655169571585, + "mean": 0.000018236681013726, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Parse form data", + "hz": 61005.265565852205, + "rme": 1.6642199569551437, + "mean": 0.000016392027650802517, + "sample": 88, + "fastest": false, + "slowest": false + }, + { + "name": "Parse API query", + "hz": 49176.92558808578, + "rme": 1.4670771376986425, + "mean": 0.000020334740084733408, + "sample": 90, + "fastest": false, + "slowest": false + }, + { + "name": "Parse empty values", + "hz": 231208.43111982575, + "rme": 1.6106295431332698, + "mean": 0.00000432510179302995, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse special characters", + "hz": 298818.6664572633, + "rme": 1.7054856848758984, + "mean": 0.0000033465111529202906, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with allowDots", + "hz": 174176.8526101155, + "rme": 1.4217008872377315, + "mean": 0.000005741291021249766, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with comma arrays", + "hz": 294126.4482630959, + "rme": 1.548132341106036, + "mean": 0.0000033998982611230546, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with depth limit", + "hz": 73297.77982796784, + "rme": 1.217286057909573, + "mean": 0.000013642978032172747, + "sample": 92, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750779015703.json b/benchmark/benchmark/results/benchmark-1750779015703.json new file mode 100644 index 00000000..5e7643b9 --- /dev/null +++ b/benchmark/benchmark/results/benchmark-1750779015703.json @@ -0,0 +1,160 @@ +{ + "timestamp": "2025-06-24T15:30:15.696Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Stringify simple object", + "hz": 1028470.3557756268, + "rme": 1.103010908079763, + "mean": 9.72317767239722e-7, + "sample": 93, + "fastest": true, + "slowest": false + }, + { + "name": "Stringify nested object", + "hz": 189946.45200316535, + "rme": 1.5140064980719254, + "mean": 0.000005264641636914258, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify arrays", + "hz": 110502.68428690724, + "rme": 1.8226669397242938, + "mean": 0.000009049553922180003, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify mixed object", + "hz": 55090.37300705769, + "rme": 1.4771198739502462, + "mean": 0.00001815199181664442, + "sample": 90, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify large object", + "hz": 15287.953031601703, + "rme": 1.4065127468491831, + "mean": 0.000065410980654696, + "sample": 91, + "fastest": false, + "slowest": true + }, + { + "name": "Stringify array indices format", + "hz": 110509.7801411243, + "rme": 1.2041164275055034, + "mean": 0.00000904897284858381, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify array brackets format", + "hz": 112548.84701467244, + "rme": 1.4592833508557594, + "mean": 0.000008885031046738621, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify array repeat format", + "hz": 122884.82974219302, + "rme": 1.533959071845941, + "mean": 0.000008137700984718424, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify array comma format", + "hz": 259587.3257615586, + "rme": 2.1451151606225607, + "mean": 0.000003852268199405622, + "sample": 85, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with allowDots", + "hz": 196407.06562437152, + "rme": 1.6198383657744195, + "mean": 0.000005091466525509321, + "sample": 84, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify without encoding", + "hz": 152322.9203178602, + "rme": 1.50989155063637, + "mean": 0.000006565000184563477, + "sample": 90, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with query prefix", + "hz": 728023.1169921589, + "rme": 1.6039311523514577, + "mean": 0.0000013735827567282734, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with custom delimiter", + "hz": 564747.9503118789, + "rme": 1.5567980461275843, + "mean": 0.0000017707014243216918, + "sample": 87, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify null values", + "hz": 873843.2646217628, + "rme": 1.952209774649348, + "mean": 0.0000011443699808487318, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with strictNullHandling", + "hz": 630102.0574284662, + "rme": 1.1043027151349902, + "mean": 0.0000015870444925717886, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify with skipNulls", + "hz": 741219.4545179476, + "rme": 1.1634869052066614, + "mean": 0.0000013491281076133524, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Stringify special characters", + "hz": 576413.1066978502, + "rme": 1.1737741267245514, + "mean": 0.000001734866866107175, + "sample": 92, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/compare-benchmarks.js b/benchmark/compare-benchmarks.js deleted file mode 100644 index 9b83c7de..00000000 --- a/benchmark/compare-benchmarks.js +++ /dev/null @@ -1,152 +0,0 @@ -'use strict'; - -/** - * Benchmark comparison script that compares two benchmark results and checks for performance regressions - * - * Usage: node compare-benchmarks.js [threshold] - * - * Arguments: - * - baseline-result.json: Path to the baseline benchmark result file - * - current-result.json: Path to the current benchmark result file - * - threshold: Optional performance regression threshold as a percentage (default: -10) - * - * Exit codes: - * - 0: No regressions detected - * - 1: Significant performance regression detected - * - 2: Error in script execution - */ - -const fs = require('fs'); -const path = require('path'); -const colors = require('colors'); - -// Check arguments -if (process.argv.length < 4) { - console.error('Usage: node compare-benchmarks.js [threshold]'); - process.exit(2); -} - -// Parse arguments -const baselinePath = process.argv[2]; -const currentPath = process.argv[3]; -const threshold = parseFloat(process.argv[4] || '-10'); // Default: 10% slowdown threshold - -// Validate files -if (!fs.existsSync(baselinePath)) { - console.error(`Error: Baseline file '${baselinePath}' does not exist`); - process.exit(2); -} - -if (!fs.existsSync(currentPath)) { - console.error(`Error: Current file '${currentPath}' does not exist`); - process.exit(2); -} - -// Read and parse results -let baseline, current; - -try { - baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8')); -} catch (error) { - console.error(`Error parsing baseline file: ${error.message}`); - process.exit(2); -} - -try { - current = JSON.parse(fs.readFileSync(currentPath, 'utf8')); -} catch (error) { - console.error(`Error parsing current file: ${error.message}`); - process.exit(2); -} - -// Compare results -let hasSevereSlowDown = false; -const failingTests = []; -const allTests = []; - -// Compare each benchmark -current.results.forEach(result => { - const baseResult = baseline.results.find(b => b.name === result.name); - if (!baseResult) return; - - const change = ((result.hz - baseResult.hz) / baseResult.hz * 100); - - // Add to all tests for summary report - allTests.push({ - name: result.name, - change: change.toFixed(2), - current: result.hz.toLocaleString(), - baseline: baseResult.hz.toLocaleString() - }); - - if (change < threshold) { - failingTests.push({ - name: result.name, - change: change.toFixed(2), - current: result.hz.toLocaleString(), - baseline: baseResult.hz.toLocaleString() - }); - hasSevereSlowDown = true; - } -}); - -// Calculate statistics -const changes = allTests.map(test => parseFloat(test.change)); -const avgChange = changes.reduce((sum, val) => sum + val, 0) / changes.length; -const minChange = Math.min(...changes); -const maxChange = Math.max(...changes); - -// Print summary -console.log(colors.blue('\nπŸ“Š Performance Comparison Summary:')); -console.log(colors.gray(`\nNode.js: ${process.version} | OS: ${process.platform}-${process.arch}`)); -console.log(colors.gray(`Baseline: ${path.basename(baselinePath)} | Current: ${path.basename(currentPath)}`)); -console.log(colors.gray(`Threshold: ${threshold}% | Average Change: ${avgChange.toFixed(2)}% | Range: ${minChange.toFixed(2)}% to ${maxChange.toFixed(2)}%`)); - -// Sort tests by change (worst first) -allTests.sort((a, b) => parseFloat(a.change) - parseFloat(b.change)); - -console.log('\nDETAILED RESULTS:'); -allTests.forEach(test => { - const changeNum = parseFloat(test.change); - - // Determine indicator icon based on change magnitude - let indicator; - if (changeNum < -5) { - indicator = colors.red('πŸ”»'); - } else if (changeNum < 0) { - indicator = colors.yellow('β–Ό'); - } else if (changeNum > 5) { - indicator = colors.green('πŸ”Ό'); - } else if (changeNum > 0) { - indicator = colors.green('β–²'); - } else { - indicator = colors.gray('βž–'); - } - - const changeText = changeNum >= 0 ? `+${test.change}%` : `${test.change}%`; - - // Determine color based on change magnitude - let changeColor; - if (changeNum < threshold) { - changeColor = colors.red; - } else if (changeNum < 0) { - changeColor = colors.yellow; - } else if (changeNum > 0) { - changeColor = colors.green; - } else { - changeColor = colors.gray; - } - - console.log(`${indicator} ${test.name}: ${changeColor(changeText)} (${test.current} vs ${test.baseline} ops/sec)`); -}); - -if (hasSevereSlowDown) { - console.log(colors.red(`\n❌ Performance regression detected! The following tests exceed the threshold of ${Math.abs(threshold)}% slowdown:`)); - failingTests.forEach(test => { - console.log(colors.red(`- ${test.name}: ${test.change}% (${test.current} vs ${test.baseline} ops/sec)`)); - }); - process.exit(1); -} else { - console.log(colors.green(`\nβœ… No significant performance regressions detected.`)); - process.exit(0); -} diff --git a/benchmark/fixtures.js b/benchmark/fixtures.js index 2af2310c..4ddc0e03 100644 --- a/benchmark/fixtures.js +++ b/benchmark/fixtures.js @@ -4,17 +4,59 @@ * Generates various test data patterns for benchmarking */ +// Load polyfills for older Node.js versions +require('./polyfills'); + +// Helper function to replace Array.from for older Node.js +function generateArray(length, mapFn) { + var arr = new Array(length); + for (var i = 0; i < length; i++) { + arr[i] = mapFn(null, i); + } + return arr; +} + +// Helper function for repeating strings +function repeatString(str, count) { + if (typeof String.prototype.repeat === 'function') { + return str.repeat(count); + } + + var result = ''; + while (count > 0) { + result += str; + count--; + } + return result; +} + // Simple flat query strings -const simpleQueries = { +var simpleQueries = { tiny: 'a=1&b=2&c=3', - small: 'name=John&age=30&city=NYC&country=USA&email=john@example.com', - medium: Array.from({length: 20}, (_, i) => `param${i}=value${i}`).join('&'), - large: Array.from({length: 100}, (_, i) => `param${i}=value${i}`).join('&'), - xlarge: Array.from({length: 500}, (_, i) => `param${i}=value${i}`).join('&') + small: 'name=John&age=30&city=NYC&country=USA&email=john@example.com' }; +// Generate medium and large query strings using a loop +simpleQueries.medium = ''; +for (var i = 0; i < 20; i++) { + if (i > 0) simpleQueries.medium += '&'; + simpleQueries.medium += 'param' + i + '=value' + i; +} + +simpleQueries.large = ''; +for (var i = 0; i < 100; i++) { + if (i > 0) simpleQueries.large += '&'; + simpleQueries.large += 'param' + i + '=value' + i; +} + +simpleQueries.xlarge = ''; +for (var i = 0; i < 500; i++) { + if (i > 0) simpleQueries.xlarge += '&'; + simpleQueries.xlarge += 'param' + i + '=value' + i; +} + // Nested object queries -const nestedQueries = { +var nestedQueries = { shallow: 'user[name]=John&user[age]=30&user[email]=john@example.com', medium: 'user[profile][name]=John&user[profile][bio]=Developer&user[settings][theme]=dark', deep: 'user[profile][personal][name][first]=John&user[profile][personal][name][last]=Doe&user[profile][personal][address][street]=123 Main St&user[profile][personal][address][city]=NYC', @@ -22,25 +64,46 @@ const nestedQueries = { }; // Array queries -const arrayQueries = { +var arrayQueries = { simple: 'colors[]=red&colors[]=green&colors[]=blue', indexed: 'items[0]=first&items[1]=second&items[2]=third&items[3]=fourth&items[4]=fifth', sparse: 'sparse[1]=second&sparse[5]=sixth&sparse[10]=tenth', - mixed: 'user[name]=John&user[hobbies][]=reading&user[hobbies][]=coding&user[addresses][0][street]=123 Main&user[addresses][0][city]=NYC', - large: Array.from({length: 50}, (_, i) => `items[]=${i}`).join('&') + mixed: 'user[name]=John&user[hobbies][]=reading&user[hobbies][]=coding&user[addresses][0][street]=123 Main&user[addresses][0][city]=NYC' }; +// Generate large array query string +arrayQueries.large = ''; +for (var i = 0; i < 50; i++) { + if (i > 0) arrayQueries.large += '&'; + arrayQueries.large += 'items[]=' + i; +} + // Edge case queries -const edgeCaseQueries = { +var edgeCaseQueries = { emptyValues: 'a=&b=&c=value&d=', - specialChars: 'special=%21%40%23%24%25%5E%26%2A%28%29&unicode=%E2%9C%93', - longValues: `long=${'x'.repeat(1000)}&short=y`, - manyParams: Array.from({length: 200}, (_, i) => `p${i}=v${i}`).join('&'), - deepNesting: Array.from({length: 10}, (_, i) => `level${i}`).reduce((acc, level) => `${acc}[${level}]`, 'deep') + '=value' + specialChars: 'special=%21%40%23%24%25%5E%26%2A%28%29&unicode=%E2%9C%93' }; +// Generate long values - repeat 'x' 1000 times +var xRepeated = repeatString('x', 1000); +edgeCaseQueries.longValues = 'long=' + xRepeated + '&short=y'; + +// Generate many params +edgeCaseQueries.manyParams = ''; +for (var i = 0; i < 200; i++) { + if (i > 0) edgeCaseQueries.manyParams += '&'; + edgeCaseQueries.manyParams += 'p' + i + '=v' + i; +} + +// Generate deep nesting +edgeCaseQueries.deepNesting = 'deep'; +for (var i = 0; i < 10; i++) { + edgeCaseQueries.deepNesting += '[level' + i + ']'; +} +edgeCaseQueries.deepNesting += '=value'; + // Mixed complexity queries (realistic scenarios) -const realWorldQueries = { +var realWorldQueries = { ecommerce: 'category=electronics&subcategory=phones&brand[]=apple&brand[]=samsung&price[min]=100&price[max]=1000&features[]=camera&features[]=5g&sort=price&order=asc&page=2&limit=20', form: 'user[name]=John%20Doe&user[email]=john%40example.com&user[age]=30&preferences[newsletter]=true&preferences[theme]=dark&address[street]=123%20Main%20St&address[city]=New%20York&address[zip]=10001', analytics: 'event=pageview×tamp=1640995200&user[id]=12345&user[session]=abc123&page[url]=/products/123&page[title]=Product%20Details&utm[source]=google&utm[medium]=cpc&utm[campaign]=summer_sale', @@ -48,19 +111,42 @@ const realWorldQueries = { }; // Performance stress tests -const stressTestQueries = { - manyKeys: Array.from({length: 1000}, (_, i) => `key${i}=value${i}`).join('&'), - deepNesting: 'a' + '[level]'.repeat(20) + '=deep', - largeArrays: Array.from({length: 100}, (_, i) => `items[]=${i}`).join('&'), - mixedComplexity: [ - ...Array.from({length: 50}, (_, i) => `simple${i}=value${i}`), - ...Array.from({length: 20}, (_, i) => `nested[${i}][prop]=value${i}`), - ...Array.from({length: 30}, (_, i) => `array[]=${i}`) - ].join('&') +var stressTestQueries = { + deepNesting: 'a' + repeatString('[level]', 20) + '=deep' }; +// Generate manyKeys +stressTestQueries.manyKeys = ''; +for (var i = 0; i < 1000; i++) { + if (i > 0) stressTestQueries.manyKeys += '&'; + stressTestQueries.manyKeys += 'key' + i + '=value' + i; +} + +// Generate largeArrays +stressTestQueries.largeArrays = ''; +for (var i = 0; i < 100; i++) { + if (i > 0) stressTestQueries.largeArrays += '&'; + stressTestQueries.largeArrays += 'items[]=' + i; +} + +// Generate mixedComplexity +stressTestQueries.mixedComplexity = ''; +// Simple items +for (var i = 0; i < 50; i++) { + if (i > 0) stressTestQueries.mixedComplexity += '&'; + stressTestQueries.mixedComplexity += 'simple' + i + '=value' + i; +} +// Nested items +for (var i = 0; i < 20; i++) { + stressTestQueries.mixedComplexity += '&nested[' + i + '][prop]=value' + i; +} +// Array items +for (var i = 0; i < 30; i++) { + stressTestQueries.mixedComplexity += '&array[]=' + i; +} + // Generate test objects for stringify benchmarks -const testObjects = { +var testObjects = { simple: { a: '1', b: '2', c: '3' }, nested: { user: { @@ -93,10 +179,42 @@ const testObjects = { push: false } } + } +}; + +// Generate large object without using Object.fromEntries +testObjects.large = {}; +for (var i = 0; i < 100; i++) { + testObjects.large['key' + i] = 'value' + i; +} + +module.exports = { + simpleQueries: simpleQueries, + nestedQueries: nestedQueries, + arrayQueries: arrayQueries, + edgeCaseQueries: edgeCaseQueries, + realWorldQueries: realWorldQueries, + stressTestQueries: stressTestQueries, + testObjects: testObjects, + + // Helper functions + generateQuery: function(type, size) { + size = size || 'medium'; + var queries = { + simple: simpleQueries, + nested: nestedQueries, + array: arrayQueries, + edge: edgeCaseQueries, + real: realWorldQueries, + stress: stressTestQueries + }; + + return queries[type] && queries[type][size] ? queries[type][size] : simpleQueries.medium; }, - large: Object.fromEntries( - Array.from({length: 100}, (_, i) => [`key${i}`, `value${i}`]) - ) + + generateObject: function(type) { + return testObjects[type] || testObjects.simple; + } }; module.exports = { @@ -107,7 +225,7 @@ module.exports = { realWorldQueries, stressTestQueries, testObjects, - + // Helper functions generateQuery: (type, size = 'medium') => { const queries = { @@ -118,10 +236,10 @@ module.exports = { real: realWorldQueries, stress: stressTestQueries }; - + return queries[type]?.[size] || simpleQueries.medium; }, - + generateObject: (type) => { return testObjects[type] || testObjects.simple; } diff --git a/benchmark/index.js b/benchmark/index.js index c88ce521..85ee9865 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -1,53 +1,99 @@ #!/usr/bin/env node 'use strict'; -const runParseBenchmarks = require('./parse'); -const runStringifyBenchmarks = require('./stringify'); -const colors = require('colors'); -const fs = require('fs'); -const path = require('path'); +var runParseBenchmarks = require('./parse'); +var runStringifyBenchmarks = require('./stringify'); +var colors = require('colors'); +var fs = require('fs'); +var path = require('path'); -async function main() { - const args = process.argv.slice(2); - const options = parseArgs(args); +// Load polyfills for older Node.js versions +require('./polyfills'); + +function main(callback) { + var args = process.argv.slice(2); + var options = parseArgs(args); console.log(colors.blue.bold('πŸš€ QS Library Benchmarks')); - console.log(colors.gray(`Node.js ${process.version} on ${process.platform}-${process.arch}\n`)); + console.log(colors.gray('Node.js ' + process.version + ' on ' + process.platform + '-' + process.arch + '\n')); if (options.baseline) { - console.log(colors.yellow(`πŸ“Š Comparing against baseline: ${options.baseline}\n`)); + console.log(colors.yellow('πŸ“Š Comparing against baseline: ' + options.baseline + '\n')); } - try { + // Step 1: Run parse benchmarks if needed + function runParse() { if (options.parse || options.all) { - await runParseBenchmarks(); - console.log('\n' + '='.repeat(60) + '\n'); + runParseBenchmarks(function(err) { + if (err) { + return handleError(err); + } + console.log('\n' + repeatString('=', 60) + '\n'); + runStringify(); + }); + } else { + runStringify(); } + } + // Step 2: Run stringify benchmarks if needed + function runStringify() { if (options.stringify || options.all) { - await runStringifyBenchmarks(); - console.log('\n' + '='.repeat(60) + '\n'); + runStringifyBenchmarks(function(err) { + if (err) { + return handleError(err); + } + console.log('\n' + repeatString('=', 60) + '\n'); + finalize(); + }); + } else { + finalize(); } + } + // Step 3: Finalize benchmarks + function finalize() { // Generate summary report if (options.summary) { generateSummaryReport(); } console.log(colors.green('βœ… Benchmarks completed successfully!')); - + if (options.save) { saveBaseline(); } - } catch (error) { + if (callback) callback(null); + } + + // Handle errors + function handleError(error) { console.error(colors.red('❌ Benchmark failed:'), error.message); - process.exit(1); + if (callback) callback(error); + else process.exit(1); } + + // Start the process + runParse(); +} + +// Helper function for repeating strings (replacement for String.repeat) +function repeatString(str, count) { + if (typeof String.prototype.repeat === 'function') { + return str.repeat(count); + } + + var result = ''; + while (count > 0) { + result += str; + count--; + } + return result; } function parseArgs(args) { - const options = { + var options = { all: true, parse: false, stringify: false, @@ -56,8 +102,8 @@ function parseArgs(args) { save: false }; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; + for (var i = 0; i < args.length; i++) { + var arg = args[i]; switch (arg) { case '--parse': options.parse = true; @@ -68,7 +114,10 @@ function parseArgs(args) { options.all = false; break; case '--baseline': - options.baseline = args[++i]; + if (i + 1 < args.length) { + options.baseline = args[i + 1]; + i++; + } break; case '--save-baseline': options.save = true; @@ -80,8 +129,8 @@ function parseArgs(args) { printHelp(); process.exit(0); default: - if (arg.startsWith('--')) { - console.error(colors.red(`Unknown option: ${arg}`)); + if (arg.indexOf('--') === 0) { + console.error(colors.red('Unknown option: ' + arg)); printHelp(); process.exit(1); } @@ -92,84 +141,112 @@ function parseArgs(args) { } function printHelp() { - console.log(` -${colors.blue.bold('QS Benchmark Suite')} - -${colors.yellow('Usage:')} - node benchmark/index.js [options] - -${colors.yellow('Options:')} - --parse Run only parse benchmarks - --stringify Run only stringify benchmarks - --baseline FILE Compare results against baseline file - --save-baseline Save current results as baseline - --no-summary Skip summary report - --help Show this help - -${colors.yellow('Examples:')} - node benchmark/index.js # Run all benchmarks - node benchmark/index.js --parse # Run only parse benchmarks - node benchmark/index.js --baseline baseline.json # Compare with baseline - node benchmark/index.js --save-baseline # Save as new baseline - STRESS_TEST=1 node benchmark/index.js # Include stress tests - -${colors.yellow('Environment Variables:')} - STRESS_TEST=1 Include stress test scenarios -`); + console.log( + '\n' + colors.blue.bold('QS Benchmark Suite') + + '\n\n' + colors.yellow('Usage:') + + '\n node benchmark/index.js [options]' + + '\n\n' + colors.yellow('Options:') + + '\n --parse Run only parse benchmarks' + + '\n --stringify Run only stringify benchmarks' + + '\n --baseline FILE Compare results against baseline file' + + '\n --save-baseline Save current results as baseline' + + '\n --no-summary Skip summary report' + + '\n --help Show this help' + + '\n\n' + colors.yellow('Examples:') + + '\n node benchmark/index.js # Run all benchmarks' + + '\n node benchmark/index.js --parse # Run only parse benchmarks' + + '\n node benchmark/index.js --baseline baseline.json # Compare with baseline' + + '\n node benchmark/index.js --save-baseline # Save as new baseline' + + '\n STRESS_TEST=1 node benchmark/index.js # Include stress tests' + + '\n\n' + colors.yellow('Environment Variables:') + + '\n STRESS_TEST=1 Include stress test scenarios' + + '\n' + ); } function generateSummaryReport() { - const resultsDir = path.join(__dirname, 'results'); + var resultsDir = path.join(__dirname, 'results'); if (!fs.existsSync(resultsDir)) return; - const files = fs.readdirSync(resultsDir) - .filter(f => f.startsWith('benchmark-') && f.endsWith('.json')) - .sort() - .slice(-5); // Last 5 runs + var files = fs.readdirSync(resultsDir) + .filter(function(f) { + return f.indexOf('benchmark-') === 0 && f.indexOf('.json') === f.length - 5; + }) + .sort(); + + // Get the last 5 runs + if (files.length > 5) { + files = files.slice(files.length - 5); + } if (files.length === 0) return; console.log(colors.blue('\nπŸ“ˆ Performance Trend (Last 5 Runs)\n')); - const trends = {}; - files.forEach(file => { - const data = JSON.parse(fs.readFileSync(path.join(resultsDir, file), 'utf8')); - data.results.forEach(result => { + var trends = {}; + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var data = JSON.parse(fs.readFileSync(path.join(resultsDir, file), 'utf8')); + + for (var j = 0; j < data.results.length; j++) { + var result = data.results[j]; if (!trends[result.name]) trends[result.name] = []; trends[result.name].push(result.hz); - }); - }); + } + } - Object.entries(trends).forEach(([testName, values]) => { - if (values.length < 2) return; - - const latest = values[values.length - 1]; - const previous = values[values.length - 2]; - const change = ((latest - previous) / previous * 100).toFixed(2); - const trend = change > 5 ? 'πŸ“ˆ' : change < -5 ? 'πŸ“‰' : 'βž–'; - - console.log(`${trend} ${testName}: ${change >= 0 ? '+' : ''}${change}%`); - }); + for (var testName in trends) { + if (trends.hasOwnProperty(testName)) { + var values = trends[testName]; + if (values.length < 2) continue; + + var latest = values[values.length - 1]; + var previous = values[values.length - 2]; + var change = ((latest - previous) / previous * 100).toFixed(2); + var trend; + + if (change > 5) { + trend = 'πŸ“ˆ'; + } else if (change < -5) { + trend = 'πŸ“‰'; + } else { + trend = 'βž–'; + } + + var changeText = (change >= 0 ? '+' : '') + change + '%'; + console.log(trend + ' ' + testName + ': ' + changeText); + } + } } function saveBaseline() { - const resultsDir = path.join(__dirname, 'results'); - const files = fs.readdirSync(resultsDir) - .filter(f => f.startsWith('benchmark-') && f.endsWith('.json')) + var resultsDir = path.join(__dirname, 'results'); + var files = fs.readdirSync(resultsDir) + .filter(function(f) { + return f.indexOf('benchmark-') === 0 && f.indexOf('.json') === f.length - 5; + }) .sort(); - + if (files.length === 0) return; - - const latest = files[files.length - 1]; - const baselinePath = path.join(__dirname, 'baseline.json'); - - fs.copyFileSync(path.join(resultsDir, latest), baselinePath); - console.log(colors.green(`πŸ’Ύ Baseline saved to ${baselinePath}`)); + + var latest = files[files.length - 1]; + var baselinePath = path.join(__dirname, 'baseline.json'); + + // Copy file - Node.js 0.8 doesn't have fs.copyFileSync + var content = fs.readFileSync(path.join(resultsDir, latest)); + fs.writeFileSync(baselinePath, content); + + console.log(colors.green('πŸ’Ύ Baseline saved to ' + baselinePath)); } if (require.main === module) { - main().catch(console.error); + main(function(err) { + if (err) { + console.error(err); + process.exit(1); + } + }); } -module.exports = { main }; +module.exports = { main: main }; diff --git a/benchmark/package.json b/benchmark/package.json index 643425e5..ab225efe 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -2,14 +2,6 @@ "name": "qs-benchmarks", "private": true, "description": "Benchmarking suite for qs library", - "scripts": { - "benchmark": "cd benchmark && npm install && node index.js", - "benchmark:parse": "cd benchmark && npm install && node index.js --parse", - "benchmark:stringify": "cd benchmark && npm install && node index.js --stringify", - "benchmark:baseline": "cd benchmark && npm install && node index.js --save-baseline", - "benchmark:compare": "cd benchmark && npm install && node index.js --baseline baseline.json", - "benchmark:stress": "cd benchmark && npm install && STRESS_TEST=1 node index.js" - }, "dependencies": { "benchmark": "^2.1.4", "table": "^6.8.1", diff --git a/benchmark/parse.js b/benchmark/parse.js index 69cc28a2..14f19ad8 100644 --- a/benchmark/parse.js +++ b/benchmark/parse.js @@ -1,108 +1,116 @@ 'use strict'; -const BenchmarkRunner = require('./runner'); -const fixtures = require('./fixtures'); -const qs = require('../lib/index'); +var BenchmarkRunner = require('./runner'); +var fixtures = require('./fixtures'); +var qs = require('../lib/index'); -async function runParseBenchmarks() { +// Ensure polyfills are loaded +require('./polyfills'); + +function runParseBenchmarks(callback) { console.log('πŸ” Running Parse Benchmarks...\n'); - - const runner = new BenchmarkRunner({ + + var runner = new BenchmarkRunner({ verbose: true, outputDir: './benchmark/results' }); // Simple parsing benchmarks runner - .add('Parse simple tiny query', () => { + .add('Parse simple tiny query', function() { qs.parse(fixtures.simpleQueries.tiny); }) - .add('Parse simple small query', () => { + .add('Parse simple small query', function() { qs.parse(fixtures.simpleQueries.small); }) - .add('Parse simple medium query', () => { + .add('Parse simple medium query', function() { qs.parse(fixtures.simpleQueries.medium); }) - .add('Parse simple large query', () => { + .add('Parse simple large query', function() { qs.parse(fixtures.simpleQueries.large); }); // Nested object parsing runner - .add('Parse shallow nested', () => { + .add('Parse shallow nested', function() { qs.parse(fixtures.nestedQueries.shallow); }) - .add('Parse medium nested', () => { + .add('Parse medium nested', function() { qs.parse(fixtures.nestedQueries.medium); }) - .add('Parse deep nested', () => { + .add('Parse deep nested', function() { qs.parse(fixtures.nestedQueries.deep); }); // Array parsing runner - .add('Parse simple array', () => { + .add('Parse simple array', function() { qs.parse(fixtures.arrayQueries.simple); }) - .add('Parse indexed array', () => { + .add('Parse indexed array', function() { qs.parse(fixtures.arrayQueries.indexed); }) - .add('Parse mixed array/object', () => { + .add('Parse mixed array/object', function() { qs.parse(fixtures.arrayQueries.mixed); }); // Real-world scenarios runner - .add('Parse e-commerce query', () => { + .add('Parse e-commerce query', function() { qs.parse(fixtures.realWorldQueries.ecommerce); }) - .add('Parse form data', () => { + .add('Parse form data', function() { qs.parse(fixtures.realWorldQueries.form); }) - .add('Parse API query', () => { + .add('Parse API query', function() { qs.parse(fixtures.realWorldQueries.api); }); // Edge cases runner - .add('Parse empty values', () => { + .add('Parse empty values', function() { qs.parse(fixtures.edgeCaseQueries.emptyValues); }) - .add('Parse special characters', () => { + .add('Parse special characters', function() { qs.parse(fixtures.edgeCaseQueries.specialChars); }); // Options testing runner - .add('Parse with allowDots', () => { + .add('Parse with allowDots', function() { qs.parse('user.name=John&user.age=30', { allowDots: true }); }) - .add('Parse with comma arrays', () => { + .add('Parse with comma arrays', function() { qs.parse('colors=red,green,blue', { comma: true }); }) - .add('Parse with depth limit', () => { + .add('Parse with depth limit', function() { qs.parse(fixtures.nestedQueries.deep, { depth: 2 }); }); // Stress tests (comment out for regular benchmarks) if (process.env.STRESS_TEST) { runner - .add('Parse many keys (stress)', () => { + .add('Parse many keys (stress)', function() { qs.parse(fixtures.stressTestQueries.manyKeys); }) - .add('Parse deep nesting (stress)', () => { + .add('Parse deep nesting (stress)', function() { qs.parse(fixtures.stressTestQueries.deepNesting); }) - .add('Parse large arrays (stress)', () => { + .add('Parse large arrays (stress)', function() { qs.parse(fixtures.stressTestQueries.largeArrays); }); } - await runner.run(); + runner.run(callback); } if (require.main === module) { - runParseBenchmarks().catch(console.error); + runParseBenchmarks(function(err) { + if (err) { + console.error(err); + process.exit(1); + } + }); } module.exports = runParseBenchmarks; diff --git a/benchmark/polyfills.js b/benchmark/polyfills.js new file mode 100644 index 00000000..110c04a6 --- /dev/null +++ b/benchmark/polyfills.js @@ -0,0 +1,187 @@ +'use strict'; + +/** + * Polyfills for older Node.js versions (0.8+) + * This file provides implementations for features not available in older Node versions + */ + +// Polyfill for Array.from (ES6) +if (!Array.from) { + Array.from = function (arrayLike, mapFn, thisArg) { + var result = []; + var length, i; + + if (arrayLike == null) { + throw new TypeError('Array.from requires an array-like object'); + } + + length = arrayLike.length >>> 0; + + for (i = 0; i < length; i++) { + if (i in arrayLike) { + if (mapFn) { + result[i] = mapFn.call(thisArg, arrayLike[i], i, arrayLike); + } else { + result[i] = arrayLike[i]; + } + } + } + + return result; + }; +} + +// Polyfill for String.prototype.repeat (ES6) +if (!String.prototype.repeat) { + String.prototype.repeat = function (count) { + if (count < 0) { + throw new RangeError('repeat count must be non-negative'); + } + if (count === Infinity) { + throw new RangeError('repeat count must be less than infinity'); + } + + count = Math.floor(count); + if (this.length === 0 || count === 0) { + return ''; + } + + var result = ''; + var pattern = this; + + while (count > 0) { + if (count & 1) { + result += pattern; + } + count >>= 1; + pattern += pattern; + } + + return result; + }; +} + +// Polyfill for Object.fromEntries (ES2019) +if (!Object.fromEntries) { + Object.fromEntries = function (entries) { + var obj = {}; + + for (var i = 0; i < entries.length; i++) { + var entry = entries[i]; + if (Object.prototype.toString.call(entry) !== '[object Array]') { + throw new TypeError('Iterator value ' + entry + ' is not an entry object'); + } + obj[entry[0]] = entry[1]; + } + + return obj; + }; +} + +// Polyfill for Object.entries (ES2017) +if (!Object.entries) { + Object.entries = function (obj) { + var ownProps = Object.keys(obj); + var i = ownProps.length; + var resArray = new Array(i); + + while (i--) { + resArray[i] = [ownProps[i], obj[ownProps[i]]]; + } + + return resArray; + }; +} + +// Simplified polyfill for Promise (ES6) for Node.js 0.8+ +// For complex usage, consider using a full Promise library like Q or Bluebird +if (typeof Promise === 'undefined') { + global.Promise = function (executor) { + var callbacks = []; + var value; + var resolved = false; + + this.then = function (onFulfilled) { + if (resolved) { + onFulfilled(value); + } else { + callbacks.push(onFulfilled); + } + return this; + }; + + function resolve(val) { + value = val; + resolved = true; + + for (var i = 0; i < callbacks.length; i++) { + callbacks[i](value); + } + callbacks = []; + } + + executor(resolve); + }; +} + +// Ensure fs.mkdirSync recursive option works (Node.js 10+) +// This is a simple version that doesn't handle all edge cases +var fs = require('fs'); +var path = require('path'); + +var originalMkdirSync = fs.mkdirSync; +fs.mkdirSync = function (dirPath, options) { + if (options && options.recursive) { + var parts = dirPath.split(path.sep); + var currentPath = ''; + + for (var i = 0; i < parts.length; i++) { + if (!parts[i] && i === 0) { + currentPath = path.sep; + continue; + } + + currentPath = path.join(currentPath, parts[i]); + + try { + fs.statSync(currentPath); + } catch (e) { + originalMkdirSync(currentPath); + } + } + return; + } + + return originalMkdirSync.apply(this, arguments); +}; + +// Ensure Array includes method (ES7) +if (!Array.prototype.includes) { + Array.prototype.includes = function (searchElement, fromIndex) { + if (this == null) { + throw new TypeError('"this" is null or not defined'); + } + + var o = Object(this); + var len = o.length >>> 0; + + if (len === 0) { + return false; + } + + var n = fromIndex | 0; + var k = Math.max(n >= 0 ? n : len + n, 0); + + while (k < len) { + if (o[k] === searchElement) { + return true; + } + k++; + } + return false; + }; +} + +module.exports = { + loaded: true +}; diff --git a/benchmark/results/benchmark-1750775607814.json b/benchmark/results/benchmark-1750775607814.json new file mode 100644 index 00000000..9bda7aea --- /dev/null +++ b/benchmark/results/benchmark-1750775607814.json @@ -0,0 +1,169 @@ +{ + "timestamp": "2025-06-24T14:33:27.757Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Parse simple tiny query", + "hz": 339368.57646296266, + "rme": 1.5877642145574582, + "mean": 0.000002946648774681518, + "sample": 93, + "fastest": true, + "slowest": false + }, + { + "name": "Parse simple small query", + "hz": 166303.78950936385, + "rme": 1.0841983813030203, + "mean": 0.000006013092082569137, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple medium query", + "hz": 38462.85790917391, + "rme": 1.4113066897602626, + "mean": 0.000025999108083996187, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple large query", + "hz": 8062.515205586793, + "rme": 1.057628921792131, + "mean": 0.00012403077383433221, + "sample": 93, + "fastest": false, + "slowest": true + }, + { + "name": "Parse shallow nested", + "hz": 169808.09843727067, + "rme": 0.8811370835416009, + "mean": 0.000005889000637795923, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse medium nested", + "hz": 132794.2803924927, + "rme": 0.6406034191302279, + "mean": 0.0000075304448131678215, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse deep nested", + "hz": 72472.98755695937, + "rme": 0.6823000589684686, + "mean": 0.00001379824447300535, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple array", + "hz": 193669.5397470639, + "rme": 0.5945380745133809, + "mean": 0.000005163434587111731, + "sample": 99, + "fastest": false, + "slowest": false + }, + { + "name": "Parse indexed array", + "hz": 111618.07377058547, + "rme": 1.389286676498194, + "mean": 0.00000895912253471918, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse mixed array/object", + "hz": 79964.95688603172, + "rme": 1.4153317502720602, + "mean": 0.000012505477886084874, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse e-commerce query", + "hz": 55869.88938147373, + "rme": 0.8232662184894164, + "mean": 0.00001789872883356015, + "sample": 86, + "fastest": false, + "slowest": false + }, + { + "name": "Parse form data", + "hz": 64758.34859642685, + "rme": 1.0831317727449514, + "mean": 0.000015442024413438744, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse API query", + "hz": 50796.577670369334, + "rme": 1.792982399893066, + "mean": 0.00001968636561481031, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Parse empty values", + "hz": 249045.98363261085, + "rme": 0.9984244240955832, + "mean": 0.000004015322734436007, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse special characters", + "hz": 326350.9418241071, + "rme": 0.8537521124373892, + "mean": 0.000003064186039760132, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with allowDots", + "hz": 188545.89300983382, + "rme": 0.9810731436902679, + "mean": 0.0000053037485146804226, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with comma arrays", + "hz": 324909.63376456907, + "rme": 0.6122238240837989, + "mean": 0.0000030777788531952375, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with depth limit", + "hz": 77198.2278999816, + "rme": 1.1472932485238967, + "mean": 0.000012953665222673309, + "sample": 98, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/runner.js b/benchmark/runner.js index 69557330..8811fcd3 100644 --- a/benchmark/runner.js +++ b/benchmark/runner.js @@ -1,167 +1,222 @@ 'use strict'; -const Benchmark = require('benchmark'); -const { table } = require('table'); -const colors = require('colors'); -const fs = require('fs'); -const path = require('path'); - -class BenchmarkRunner { - constructor(options = {}) { - this.options = { - outputDir: options.outputDir || path.join(__dirname, 'results'), - baseline: options.baseline || null, - verbose: options.verbose || false, - ...options - }; - this.results = []; - this.suite = new Benchmark.Suite(); - - // Ensure output directory exists - if (!fs.existsSync(this.options.outputDir)) { - fs.mkdirSync(this.options.outputDir, { recursive: true }); +var Benchmark = require('benchmark'); +var table = require('table').table; +var colors = require('colors'); +var fs = require('fs'); +var path = require('path'); + +// Load polyfills for older Node.js versions +require('./polyfills'); + +function BenchmarkRunner(options) { + options = options || {}; + + this.options = { + outputDir: options.outputDir || path.join(__dirname, 'results'), + baseline: options.baseline || null, + verbose: options.verbose || false + }; + + // Copy any other options + for (var key in options) { + if (options.hasOwnProperty(key) && !this.options.hasOwnProperty(key)) { + this.options[key] = options[key]; } } - add(name, fn, options = {}) { - this.suite.add(name, fn, { - onComplete: (event) => { - const benchmark = event.target; - this.results.push({ - name: benchmark.name, - hz: benchmark.hz, - rme: benchmark.stats.rme, - mean: benchmark.stats.mean, - sample: benchmark.stats.sample.length, - fastest: false, - slowest: false - }); - }, - ...options - }); - return this; - } + this.results = []; + this.suite = new Benchmark.Suite(); - async run() { - return new Promise((resolve, reject) => { - this.suite - .on('start', () => { - if (this.options.verbose) { - console.log(colors.blue('πŸš€ Starting benchmarks...')); - } - }) - .on('cycle', (event) => { - if (this.options.verbose) { - console.log(colors.gray(String(event.target))); - } - }) - .on('complete', () => { - this._markFastestSlowest(); - this._generateReport(); - resolve(this.results); - }) - .on('error', reject) - .run({ async: true }); - }); + // Ensure output directory exists + if (!fs.existsSync(this.options.outputDir)) { + fs.mkdirSync(this.options.outputDir, { recursive: true }); } +} + +BenchmarkRunner.prototype.add = function(name, fn, options) { + options = options || {}; + var self = this; + + var completeOptions = { + onComplete: function(event) { + var benchmark = event.target; + self.results.push({ + name: benchmark.name, + hz: benchmark.hz, + rme: benchmark.stats.rme, + mean: benchmark.stats.mean, + sample: benchmark.stats.sample.length, + fastest: false, + slowest: false + }); + } + }; - _markFastestSlowest() { - if (this.results.length === 0) return; - - const sorted = [...this.results].sort((a, b) => b.hz - a.hz); - sorted[0].fastest = true; - sorted[sorted.length - 1].slowest = true; + // Copy any other options + for (var key in options) { + if (options.hasOwnProperty(key)) { + completeOptions[key] = options[key]; + } } - _generateReport() { - const timestamp = new Date().toISOString(); - const nodeVersion = process.version; - const platform = `${process.platform}-${process.arch}`; + this.suite.add(name, fn, completeOptions); + return this; +}; + +BenchmarkRunner.prototype.run = function(callback) { + var self = this; - // Console output - this._printConsoleReport(); + // For backward compatibility, if no callback is provided, return a Promise + if (!callback && typeof Promise !== 'undefined') { + return new Promise(function(resolve, reject) { + self._runWithCallbacks(function(err, results) { + if (err) return reject(err); + resolve(results); + }); + }); + } - // JSON output for CI/regression detection - const jsonReport = { - timestamp, - nodeVersion, - platform, - results: this.results - }; + return this._runWithCallbacks(callback || function() {}); +}; + +BenchmarkRunner.prototype._runWithCallbacks = function(callback) { + var self = this; + + this.suite + .on('start', function() { + if (self.options.verbose) { + console.log(colors.blue('πŸš€ Starting benchmarks...')); + } + }) + .on('cycle', function(event) { + if (self.options.verbose) { + console.log(colors.gray(String(event.target))); + } + }) + .on('complete', function() { + self._markFastestSlowest(); + self._generateReport(); + callback(null, self.results); + }) + .on('error', function(err) { + callback(err); + }) + .run({ async: true }); + + return this; +}; + +BenchmarkRunner.prototype._markFastestSlowest = function() { + if (this.results.length === 0) return; + + var sorted = this.results.slice().sort(function(a, b) { + return b.hz - a.hz; + }); + sorted[0].fastest = true; + sorted[sorted.length - 1].slowest = true; +}; + +BenchmarkRunner.prototype._generateReport = function() { + var timestamp = new Date().toISOString(); + var nodeVersion = process.version; + var platform = process.platform + '-' + process.arch; + + // Console output + this._printConsoleReport(); + + // JSON output for CI/regression detection + var jsonReport = { + timestamp: timestamp, + nodeVersion: nodeVersion, + platform: platform, + results: this.results + }; + + var jsonPath = path.join(this.options.outputDir, 'benchmark-' + Date.now() + '.json'); + fs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2)); + + // Baseline comparison if available + if (this.options.baseline) { + this._compareWithBaseline(jsonReport); + } +}; + +BenchmarkRunner.prototype._printConsoleReport = function() { + console.log(colors.green('\nπŸ“Š Benchmark Results\n')); + + var tableData = [ + ['Test Name', 'Ops/sec', 'RME', 'Samples', 'Status'] + ]; + + for (var i = 0; i < this.results.length; i++) { + var result = this.results[i]; + var opsPerSec = result.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }); + var rme = 'Β±' + result.rme.toFixed(2) + '%'; + var status = result.fastest ? 'πŸš€ Fastest' : + result.slowest ? '🐌 Slowest' : ''; + + tableData.push([ + result.name, + opsPerSec, + rme, + result.sample.toString(), + status + ]); + } - const jsonPath = path.join(this.options.outputDir, `benchmark-${Date.now()}.json`); - fs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2)); + console.log(table(tableData)); +}; - // Baseline comparison if available - if (this.options.baseline) { - this._compareWithBaseline(jsonReport); - } +BenchmarkRunner.prototype._compareWithBaseline = function(currentReport) { + if (!fs.existsSync(this.options.baseline)) { + console.log(colors.yellow('⚠️ Baseline file not found, skipping comparison')); + return; } - _printConsoleReport() { - console.log(colors.green('\nπŸ“Š Benchmark Results\n')); - - const tableData = [ - ['Test Name', 'Ops/sec', 'RME', 'Samples', 'Status'] + try { + var baseline = JSON.parse(fs.readFileSync(this.options.baseline, 'utf8')); + console.log(colors.blue('\nπŸ“ˆ Performance Comparison\n')); + + var comparisonData = [ + ['Test Name', 'Current', 'Baseline', 'Change', 'Status'] ]; - this.results.forEach(result => { - const opsPerSec = result.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }); - const rme = `Β±${result.rme.toFixed(2)}%`; - const status = result.fastest ? 'πŸš€ Fastest' : - result.slowest ? '🐌 Slowest' : ''; - - tableData.push([ - result.name, - opsPerSec, - rme, - result.sample.toString(), + for (var i = 0; i < currentReport.results.length; i++) { + var current = currentReport.results[i]; + var baselineResult = null; + + for (var j = 0; j < baseline.results.length; j++) { + if (baseline.results[j].name === current.name) { + baselineResult = baseline.results[j]; + break; + } + } + + if (!baselineResult) continue; + + var change = ((current.hz - baselineResult.hz) / baselineResult.hz * 100).toFixed(2); + var changeStr = (change >= 0 ? '+' : '') + change + '%'; + var status = Math.abs(change) < 5 ? 'βž– No change' : + change > 5 ? 'βœ… Improved' : + '⚠️ Regression'; + + comparisonData.push([ + current.name, + current.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }), + baselineResult.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }), + changeStr, status ]); - }); - - console.log(table(tableData)); - } - - _compareWithBaseline(currentReport) { - if (!fs.existsSync(this.options.baseline)) { - console.log(colors.yellow('⚠️ Baseline file not found, skipping comparison')); - return; } - try { - const baseline = JSON.parse(fs.readFileSync(this.options.baseline, 'utf8')); - console.log(colors.blue('\nπŸ“ˆ Performance Comparison\n')); - - const comparisonData = [ - ['Test Name', 'Current', 'Baseline', 'Change', 'Status'] - ]; - - currentReport.results.forEach(current => { - const baselineResult = baseline.results.find(b => b.name === current.name); - if (!baselineResult) return; - - const change = ((current.hz - baselineResult.hz) / baselineResult.hz * 100).toFixed(2); - const changeStr = `${change >= 0 ? '+' : ''}${change}%`; - const status = Math.abs(change) < 5 ? 'βž– No change' : - change > 5 ? 'βœ… Improved' : - '⚠️ Regression'; - - comparisonData.push([ - current.name, - current.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }), - baselineResult.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }), - changeStr, - status - ]); - }); - - console.log(table(comparisonData)); - } catch (error) { - console.log(colors.red(`❌ Error comparing with baseline: ${error.message}`)); - } + console.log(table(comparisonData)); + } catch (error) { + console.log(colors.red('❌ Error comparing with baseline: ' + error.message)); } -} +}; + +module.exports = BenchmarkRunner; module.exports = BenchmarkRunner; diff --git a/benchmark/stringify.js b/benchmark/stringify.js index 47cffa9e..a5240dac 100644 --- a/benchmark/stringify.js +++ b/benchmark/stringify.js @@ -1,67 +1,70 @@ 'use strict'; -const BenchmarkRunner = require('./runner'); -const fixtures = require('./fixtures'); -const qs = require('../lib/index'); +var BenchmarkRunner = require('./runner'); +var fixtures = require('./fixtures'); +var qs = require('../lib/index'); -async function runStringifyBenchmarks() { +// Ensure polyfills are loaded +require('./polyfills'); + +function runStringifyBenchmarks(callback) { console.log('πŸ”— Running Stringify Benchmarks...\n'); - - const runner = new BenchmarkRunner({ + + var runner = new BenchmarkRunner({ verbose: true, outputDir: './benchmark/results' }); // Simple object stringifying runner - .add('Stringify simple object', () => { + .add('Stringify simple object', function() { qs.stringify(fixtures.testObjects.simple); }) - .add('Stringify nested object', () => { + .add('Stringify nested object', function() { qs.stringify(fixtures.testObjects.nested); }) - .add('Stringify arrays', () => { + .add('Stringify arrays', function() { qs.stringify(fixtures.testObjects.arrays); }) - .add('Stringify mixed object', () => { + .add('Stringify mixed object', function() { qs.stringify(fixtures.testObjects.mixed); }) - .add('Stringify large object', () => { + .add('Stringify large object', function() { qs.stringify(fixtures.testObjects.large); }); // Different array formats runner - .add('Stringify array indices format', () => { + .add('Stringify array indices format', function() { qs.stringify(fixtures.testObjects.arrays, { arrayFormat: 'indices' }); }) - .add('Stringify array brackets format', () => { + .add('Stringify array brackets format', function() { qs.stringify(fixtures.testObjects.arrays, { arrayFormat: 'brackets' }); }) - .add('Stringify array repeat format', () => { + .add('Stringify array repeat format', function() { qs.stringify(fixtures.testObjects.arrays, { arrayFormat: 'repeat' }); }) - .add('Stringify array comma format', () => { + .add('Stringify array comma format', function() { qs.stringify(fixtures.testObjects.arrays, { arrayFormat: 'comma' }); }); // Different options runner - .add('Stringify with allowDots', () => { + .add('Stringify with allowDots', function() { qs.stringify(fixtures.testObjects.nested, { allowDots: true }); }) - .add('Stringify without encoding', () => { + .add('Stringify without encoding', function() { qs.stringify(fixtures.testObjects.mixed, { encode: false }); }) - .add('Stringify with query prefix', () => { + .add('Stringify with query prefix', function() { qs.stringify(fixtures.testObjects.simple, { addQueryPrefix: true }); }) - .add('Stringify with custom delimiter', () => { + .add('Stringify with custom delimiter', function() { qs.stringify(fixtures.testObjects.simple, { delimiter: ';' }); }); // Edge cases - const edgeObjects = { + var edgeObjects = { nullValues: { a: null, b: '', c: 'value' }, undefinedValues: { a: undefined, b: 'value', c: null }, emptyArrays: { arr: [], obj: {}, value: 'test' }, @@ -69,24 +72,29 @@ async function runStringifyBenchmarks() { }; runner - .add('Stringify null values', () => { + .add('Stringify null values', function() { qs.stringify(edgeObjects.nullValues); }) - .add('Stringify with strictNullHandling', () => { + .add('Stringify with strictNullHandling', function() { qs.stringify(edgeObjects.nullValues, { strictNullHandling: true }); }) - .add('Stringify with skipNulls', () => { + .add('Stringify with skipNulls', function() { qs.stringify(edgeObjects.nullValues, { skipNulls: true }); }) - .add('Stringify special characters', () => { + .add('Stringify special characters', function() { qs.stringify(edgeObjects.specialChars); }); - await runner.run(); + runner.run(callback); } if (require.main === module) { - runStringifyBenchmarks().catch(console.error); + runStringifyBenchmarks(function(err) { + if (err) { + console.error(err); + process.exit(1); + } + }); } module.exports = runStringifyBenchmarks; diff --git a/package.json b/package.json index 1a4ace26..4d534f27 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,12 @@ "readme": "evalmd README.md", "postlint": "eclint check $(git ls-files | xargs find 2> /dev/null | grep -vE 'node_modules|\\.git' | grep -v dist/)", "lint": "eslint --ext=js,mjs .", + "benchmark": "cd benchmark && npm install && node index.js", + "benchmark:parse": "cd benchmark && npm install && node index.js --parse", + "benchmark:stringify": "cd benchmark && npm install && node index.js --stringify", + "benchmark:baseline": "cd benchmark && npm install && node index.js --save-baseline", + "benchmark:compare": "cd benchmark && npm install && node index.js --baseline benchmark/baseline.json", + "benchmark:stress": "cd benchmark && npm install && STRESS_TEST=1 node index.js", "dist": "mkdirp dist && browserify --standalone Qs -g unassertify -g @browserify/envify -g [@browserify/uglifyify --mangle.keep_fnames --compress.keep_fnames --format.indent_level=1 --compress.arrows=false --compress.passes=4 --compress.typeofs=false] -p common-shakeify -p bundle-collapser/plugin lib/index.js > dist/qs.js" }, "license": "BSD-3-Clause", From 0cb124ffaac27136e821df03ef4dfc3580cb496f Mon Sep 17 00:00:00 2001 From: Mohamed Date: Wed, 25 Jun 2025 12:53:50 +0300 Subject: [PATCH 08/14] feat(benchmark): enhance performance benchmarking workflow and update query retrieval logic --- .github/workflows/benchmark.yml | 107 ++++++----- .../results/benchmark-1750844444618.json | 169 ++++++++++++++++++ benchmark/fixtures.js | 2 +- benchmark/package.json | 1 - benchmark/runner.js | 4 +- 5 files changed, 233 insertions(+), 50 deletions(-) create mode 100644 benchmark/benchmark/results/benchmark-1750844444618.json diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 9cdb7bc7..8dffe86c 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,52 +1,65 @@ name: Performance Benchmark on: - push: - branches: [ main ] - pull_request: - branches: [ main ] + push: + branches: [main] + pull_request: + branches: [main] jobs: - benchmark: - name: Run Performance Benchmarks - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [0.8, 0.9, 0.10, 0.11, 0.12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] - fail-fast: false - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install dependencies - uses: ljharb/actions/node/install@main - with: - node-version: ${{ matrix.node-version }} - - - name: Install benchmark dependencies - run: cd benchmark && npm install - - # For PRs: Run benchmarks and compare against committed baseline - - name: Run benchmarks and compare against baseline - if: ${{ github.event_name == 'pull_request' }} - run: | - echo "==== Running benchmarks and comparing against baseline ====" - # Run benchmarks using the compare script that uses the committed baseline - npm run benchmark:compare - - # Print the Node.js version for debugging - echo "Benchmark completed on Node.js version: $(node --version)" - - # For pushes to main: Run benchmarks against the committed baseline - - name: Run benchmarks on main branch - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - run: | - # Run benchmarks against the committed baseline - npm run benchmark:compare - - # Print the Node.js version for debugging - echo "Benchmark completed on Node.js version: $(node --version)" + matrix: + runs-on: ubuntu-latest + outputs: + latest: ${{ steps.set-matrix.outputs.requireds }} + nonlatest: ${{ steps.set-matrix.outputs.optionals }} + steps: + - uses: ljharb/actions/node/matrix@main + id: set-matrix + with: + versionsAsRoot: true + type: majors + preset: '>= 0.8' + + benchmark: + needs: [matrix] + name: Run Performance Benchmarks + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ${{ fromJson(needs.matrix.outputs.latest) }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install dependencies + uses: ljharb/actions/node/install@main + with: + node-version: ${{ matrix.node-version }} + + - name: Install benchmark dependencies + run: cd benchmark && npm install + + # For PRs: Run benchmarks and compare against committed baseline + - name: Run benchmarks and compare against baseline + if: ${{ github.event_name == 'pull_request' }} + run: | + echo "==== Running benchmarks and comparing against baseline ====" + # Run benchmarks using the compare script that uses the committed baseline + npm run benchmark:compare + + # Print the Node.js version for debugging + echo "Benchmark completed on Node.js version: $(node --version)" + + # For pushes to main: Run benchmarks against the committed baseline + - name: Run benchmarks on main branch + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + run: | + # Run benchmarks against the committed baseline + npm run benchmark:compare + + # Print the Node.js version for debugging + echo "Benchmark completed on Node.js version: $(node --version)" diff --git a/benchmark/benchmark/results/benchmark-1750844444618.json b/benchmark/benchmark/results/benchmark-1750844444618.json new file mode 100644 index 00000000..b9845283 --- /dev/null +++ b/benchmark/benchmark/results/benchmark-1750844444618.json @@ -0,0 +1,169 @@ +{ + "timestamp": "2025-06-25T09:40:44.605Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Parse simple tiny query", + "hz": 700446.7570730115, + "rme": 0.47019748081668106, + "mean": 0.0000014276602609722189, + "sample": 96, + "fastest": true, + "slowest": false + }, + { + "name": "Parse simple small query", + "hz": 336434.24503359693, + "rme": 0.34792446081693845, + "mean": 0.0000029723490243989227, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple medium query", + "hz": 77657.33933201671, + "rme": 1.2290131874191983, + "mean": 0.000012877082946720507, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple large query", + "hz": 15483.077919643303, + "rme": 1.0394997175367129, + "mean": 0.00006458664131188703, + "sample": 94, + "fastest": false, + "slowest": true + }, + { + "name": "Parse shallow nested", + "hz": 325546.5696069361, + "rme": 0.8400644965677532, + "mean": 0.0000030717571412513945, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Parse medium nested", + "hz": 250218.25448331604, + "rme": 0.6073196646741952, + "mean": 0.000003996510974248994, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Parse deep nested", + "hz": 136491.5894729609, + "rme": 0.7634530804662536, + "mean": 0.000007326458750032366, + "sample": 91, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple array", + "hz": 360522.3563161753, + "rme": 1.0817812119469699, + "mean": 0.0000027737530904269574, + "sample": 90, + "fastest": false, + "slowest": false + }, + { + "name": "Parse indexed array", + "hz": 213839.7258363581, + "rme": 0.8390830972300877, + "mean": 0.000004676399560880727, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse mixed array/object", + "hz": 152010.30442415894, + "rme": 0.681147003731818, + "mean": 0.00000657850139691629, + "sample": 94, + "fastest": false, + "slowest": false + }, + { + "name": "Parse e-commerce query", + "hz": 106558.1082903929, + "rme": 0.6626615850329857, + "mean": 0.000009384550983908169, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse form data", + "hz": 122768.06955488255, + "rme": 0.8265255924563661, + "mean": 0.000008145440452274583, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse API query", + "hz": 97874.83017688894, + "rme": 0.8274896683987512, + "mean": 0.000010217131393154936, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse empty values", + "hz": 491059.0612562051, + "rme": 0.48966978256552796, + "mean": 0.0000020364149221518186, + "sample": 96, + "fastest": false, + "slowest": false + }, + { + "name": "Parse special characters", + "hz": 645302.8387411962, + "rme": 0.3827488929620588, + "mean": 0.0000015496600045193012, + "sample": 98, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with allowDots", + "hz": 367165.535073916, + "rme": 0.5874425201490753, + "mean": 0.0000027235671774004737, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with comma arrays", + "hz": 618980.4478028473, + "rme": 1.0456187026653216, + "mean": 0.0000016155599155831688, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with depth limit", + "hz": 146680.99110890136, + "rme": 0.6964193009900447, + "mean": 0.000006817515974224384, + "sample": 95, + "fastest": false, + "slowest": false + } + ] +} \ No newline at end of file diff --git a/benchmark/fixtures.js b/benchmark/fixtures.js index 4ddc0e03..29c09f85 100644 --- a/benchmark/fixtures.js +++ b/benchmark/fixtures.js @@ -237,7 +237,7 @@ module.exports = { stress: stressTestQueries }; - return queries[type]?.[size] || simpleQueries.medium; + return (queries[type] && queries[type][size]) ? queries[type][size] : simpleQueries.medium; }, generateObject: (type) => { diff --git a/benchmark/package.json b/benchmark/package.json index ab225efe..51e93469 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -4,7 +4,6 @@ "description": "Benchmarking suite for qs library", "dependencies": { "benchmark": "^2.1.4", - "table": "^6.8.1", "colors": "=1.4.0" } } diff --git a/benchmark/runner.js b/benchmark/runner.js index 8811fcd3..0f43de64 100644 --- a/benchmark/runner.js +++ b/benchmark/runner.js @@ -1,7 +1,6 @@ 'use strict'; var Benchmark = require('benchmark'); -var table = require('table').table; var colors = require('colors'); var fs = require('fs'); var path = require('path'); @@ -9,6 +8,9 @@ var path = require('path'); // Load polyfills for older Node.js versions require('./polyfills'); +function table(data) { + return data.map(row => row.join('\t')).join('\n'); +}; function BenchmarkRunner(options) { options = options || {}; From 704f6e77605554d898b6bbaf8d50281e2f8d3505 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Wed, 25 Jun 2025 22:23:44 +0300 Subject: [PATCH 09/14] Refactor benchmark suite and update results - Deleted outdated benchmark result files to clean up the repository. - Updated the `generateQuery` function in fixtures to handle default size parameter more gracefully. - Added new dependencies for polyfills to support ES6+ features in older Node.js versions. - Removed custom polyfills for Array.from, String.prototype.repeat, Object.fromEntries, and Object.entries, replacing them with well-maintained npm packages. - Updated benchmark runner to improve code readability and maintainability. - Adjusted benchmark commands in package.json for consistency and clarity. - Added new benchmark result file with updated performance metrics for various parsing scenarios. --- .../results/benchmark-1750778180061.json | 169 ------------------ .../results/benchmark-1750778282626.json | 160 ----------------- .../results/benchmark-1750779015703.json | 160 ----------------- .../results/benchmark-1750844444618.json | 169 ------------------ ...4140.json => benchmark-1750879162316.json} | 146 +++++++-------- benchmark/fixtures.js | 3 +- benchmark/package.json | 9 +- benchmark/polyfills.js | 161 ++--------------- .../results/benchmark-1750775607814.json | 169 ------------------ benchmark/runner.js | 82 ++++----- benchmark/stringify.js | 2 +- package.json | 12 +- 12 files changed, 138 insertions(+), 1104 deletions(-) delete mode 100644 benchmark/benchmark/results/benchmark-1750778180061.json delete mode 100644 benchmark/benchmark/results/benchmark-1750778282626.json delete mode 100644 benchmark/benchmark/results/benchmark-1750779015703.json delete mode 100644 benchmark/benchmark/results/benchmark-1750844444618.json rename benchmark/benchmark/results/{benchmark-1750778914140.json => benchmark-1750879162316.json} (51%) delete mode 100644 benchmark/results/benchmark-1750775607814.json diff --git a/benchmark/benchmark/results/benchmark-1750778180061.json b/benchmark/benchmark/results/benchmark-1750778180061.json deleted file mode 100644 index a4c88d59..00000000 --- a/benchmark/benchmark/results/benchmark-1750778180061.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "timestamp": "2025-06-24T15:16:20.033Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Parse simple tiny query", - "hz": 330180.9352747842, - "rme": 1.2821140616138376, - "mean": 0.000003028642459831234, - "sample": 91, - "fastest": true, - "slowest": false - }, - { - "name": "Parse simple small query", - "hz": 164671.8191954346, - "rme": 1.4978267342248865, - "mean": 0.000006072684475618669, - "sample": 89, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple medium query", - "hz": 40605.79161576655, - "rme": 1.5709717945711017, - "mean": 0.000024627028810631943, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple large query", - "hz": 8067.461185172645, - "rme": 0.8158292309924179, - "mean": 0.00012395473334757666, - "sample": 97, - "fastest": false, - "slowest": true - }, - { - "name": "Parse shallow nested", - "hz": 161603.80309589015, - "rme": 1.7450685612920416, - "mean": 0.000006187973184063214, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Parse medium nested", - "hz": 129486.0346918172, - "rme": 1.2327906662777717, - "mean": 0.000007722840554813858, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Parse deep nested", - "hz": 70654.74651251831, - "rme": 1.0509502882828092, - "mean": 0.000014153330800257338, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple array", - "hz": 179612.67386512936, - "rme": 1.8471490503828525, - "mean": 0.0000055675358452204615, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Parse indexed array", - "hz": 108637.71584793365, - "rme": 1.371249631410273, - "mean": 0.000009204906345782863, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Parse mixed array/object", - "hz": 76987.39594733487, - "rme": 1.7231392056956119, - "mean": 0.000012989139166157467, - "sample": 90, - "fastest": false, - "slowest": false - }, - { - "name": "Parse e-commerce query", - "hz": 52859.8666736335, - "rme": 1.6351653566587596, - "mean": 0.000018917944045795332, - "sample": 88, - "fastest": false, - "slowest": false - }, - { - "name": "Parse form data", - "hz": 62734.963135179736, - "rme": 1.3567458992443153, - "mean": 0.00001594007472109651, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Parse API query", - "hz": 50810.20825889967, - "rme": 1.0367452114983366, - "mean": 0.00001968108445658348, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Parse empty values", - "hz": 247938.09095715787, - "rme": 1.3576053106540886, - "mean": 0.000004033264901490242, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Parse special characters", - "hz": 312613.0302993482, - "rme": 2.004716867955466, - "mean": 0.0000031988429882223147, - "sample": 90, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with allowDots", - "hz": 179223.81655432936, - "rme": 1.4226944866292626, - "mean": 0.0000055796155847225976, - "sample": 86, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with comma arrays", - "hz": 318063.30084301333, - "rme": 1.105069678634199, - "mean": 0.0000031440282401318926, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with depth limit", - "hz": 75213.73776598606, - "rme": 1.1452004940278429, - "mean": 0.000013295443488147326, - "sample": 93, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750778282626.json b/benchmark/benchmark/results/benchmark-1750778282626.json deleted file mode 100644 index 27baf0ac..00000000 --- a/benchmark/benchmark/results/benchmark-1750778282626.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "timestamp": "2025-06-24T15:18:02.620Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Stringify simple object", - "hz": 976109.2038787514, - "rme": 1.4366934996007836, - "mean": 0.0000010244755361657426, - "sample": 89, - "fastest": true, - "slowest": false - }, - { - "name": "Stringify nested object", - "hz": 194985.4588845839, - "rme": 1.4795812503560726, - "mean": 0.000005128587566070358, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify arrays", - "hz": 114848.53681820467, - "rme": 1.0304037149913832, - "mean": 0.00000870712007052309, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify mixed object", - "hz": 59549.669724762505, - "rme": 0.7216230963328606, - "mean": 0.000016792704386472365, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify large object", - "hz": 16029.81031807559, - "rme": 0.8349993480545844, - "mean": 0.00006238376999834967, - "sample": 94, - "fastest": false, - "slowest": true - }, - { - "name": "Stringify array indices format", - "hz": 113602.64926783308, - "rme": 0.373741414185561, - "mean": 0.000008802611615529928, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify array brackets format", - "hz": 117192.3740575468, - "rme": 0.6084953120963555, - "mean": 0.000008532978430055137, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify array repeat format", - "hz": 127845.939039929, - "rme": 0.874105890944508, - "mean": 0.000007821914466033049, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify array comma format", - "hz": 295392.78285483853, - "rme": 0.9042362757667536, - "mean": 0.0000033853230615028887, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with allowDots", - "hz": 205293.44623108386, - "rme": 1.4258567729056548, - "mean": 0.000004871076102811256, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify without encoding", - "hz": 154230.07211793793, - "rme": 1.735820829004054, - "mean": 0.000006483819830125682, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with query prefix", - "hz": 768363.2047420949, - "rme": 0.9492095133249806, - "mean": 0.000001301467839464873, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with custom delimiter", - "hz": 597488.6919134173, - "rme": 0.9812478112745247, - "mean": 0.0000016736718427215205, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify null values", - "hz": 930733.0625960926, - "rme": 0.8599816731412604, - "mean": 0.0000010744219155712609, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with strictNullHandling", - "hz": 646752.215555868, - "rme": 0.699192956942562, - "mean": 0.0000015461872042301766, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with skipNulls", - "hz": 728271.5695564854, - "rme": 1.2819874334336552, - "mean": 0.0000013731141538437319, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify special characters", - "hz": 606082.0112197315, - "rme": 1.437039782903162, - "mean": 0.0000016499417265124139, - "sample": 88, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750779015703.json b/benchmark/benchmark/results/benchmark-1750779015703.json deleted file mode 100644 index 5e7643b9..00000000 --- a/benchmark/benchmark/results/benchmark-1750779015703.json +++ /dev/null @@ -1,160 +0,0 @@ -{ - "timestamp": "2025-06-24T15:30:15.696Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Stringify simple object", - "hz": 1028470.3557756268, - "rme": 1.103010908079763, - "mean": 9.72317767239722e-7, - "sample": 93, - "fastest": true, - "slowest": false - }, - { - "name": "Stringify nested object", - "hz": 189946.45200316535, - "rme": 1.5140064980719254, - "mean": 0.000005264641636914258, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify arrays", - "hz": 110502.68428690724, - "rme": 1.8226669397242938, - "mean": 0.000009049553922180003, - "sample": 89, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify mixed object", - "hz": 55090.37300705769, - "rme": 1.4771198739502462, - "mean": 0.00001815199181664442, - "sample": 90, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify large object", - "hz": 15287.953031601703, - "rme": 1.4065127468491831, - "mean": 0.000065410980654696, - "sample": 91, - "fastest": false, - "slowest": true - }, - { - "name": "Stringify array indices format", - "hz": 110509.7801411243, - "rme": 1.2041164275055034, - "mean": 0.00000904897284858381, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify array brackets format", - "hz": 112548.84701467244, - "rme": 1.4592833508557594, - "mean": 0.000008885031046738621, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify array repeat format", - "hz": 122884.82974219302, - "rme": 1.533959071845941, - "mean": 0.000008137700984718424, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify array comma format", - "hz": 259587.3257615586, - "rme": 2.1451151606225607, - "mean": 0.000003852268199405622, - "sample": 85, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with allowDots", - "hz": 196407.06562437152, - "rme": 1.6198383657744195, - "mean": 0.000005091466525509321, - "sample": 84, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify without encoding", - "hz": 152322.9203178602, - "rme": 1.50989155063637, - "mean": 0.000006565000184563477, - "sample": 90, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with query prefix", - "hz": 728023.1169921589, - "rme": 1.6039311523514577, - "mean": 0.0000013735827567282734, - "sample": 89, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with custom delimiter", - "hz": 564747.9503118789, - "rme": 1.5567980461275843, - "mean": 0.0000017707014243216918, - "sample": 87, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify null values", - "hz": 873843.2646217628, - "rme": 1.952209774649348, - "mean": 0.0000011443699808487318, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with strictNullHandling", - "hz": 630102.0574284662, - "rme": 1.1043027151349902, - "mean": 0.0000015870444925717886, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify with skipNulls", - "hz": 741219.4545179476, - "rme": 1.1634869052066614, - "mean": 0.0000013491281076133524, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Stringify special characters", - "hz": 576413.1066978502, - "rme": 1.1737741267245514, - "mean": 0.000001734866866107175, - "sample": 92, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750844444618.json b/benchmark/benchmark/results/benchmark-1750844444618.json deleted file mode 100644 index b9845283..00000000 --- a/benchmark/benchmark/results/benchmark-1750844444618.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "timestamp": "2025-06-25T09:40:44.605Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Parse simple tiny query", - "hz": 700446.7570730115, - "rme": 0.47019748081668106, - "mean": 0.0000014276602609722189, - "sample": 96, - "fastest": true, - "slowest": false - }, - { - "name": "Parse simple small query", - "hz": 336434.24503359693, - "rme": 0.34792446081693845, - "mean": 0.0000029723490243989227, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple medium query", - "hz": 77657.33933201671, - "rme": 1.2290131874191983, - "mean": 0.000012877082946720507, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple large query", - "hz": 15483.077919643303, - "rme": 1.0394997175367129, - "mean": 0.00006458664131188703, - "sample": 94, - "fastest": false, - "slowest": true - }, - { - "name": "Parse shallow nested", - "hz": 325546.5696069361, - "rme": 0.8400644965677532, - "mean": 0.0000030717571412513945, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Parse medium nested", - "hz": 250218.25448331604, - "rme": 0.6073196646741952, - "mean": 0.000003996510974248994, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Parse deep nested", - "hz": 136491.5894729609, - "rme": 0.7634530804662536, - "mean": 0.000007326458750032366, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple array", - "hz": 360522.3563161753, - "rme": 1.0817812119469699, - "mean": 0.0000027737530904269574, - "sample": 90, - "fastest": false, - "slowest": false - }, - { - "name": "Parse indexed array", - "hz": 213839.7258363581, - "rme": 0.8390830972300877, - "mean": 0.000004676399560880727, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse mixed array/object", - "hz": 152010.30442415894, - "rme": 0.681147003731818, - "mean": 0.00000657850139691629, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Parse e-commerce query", - "hz": 106558.1082903929, - "rme": 0.6626615850329857, - "mean": 0.000009384550983908169, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse form data", - "hz": 122768.06955488255, - "rme": 0.8265255924563661, - "mean": 0.000008145440452274583, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse API query", - "hz": 97874.83017688894, - "rme": 0.8274896683987512, - "mean": 0.000010217131393154936, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse empty values", - "hz": 491059.0612562051, - "rme": 0.48966978256552796, - "mean": 0.0000020364149221518186, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse special characters", - "hz": 645302.8387411962, - "rme": 0.3827488929620588, - "mean": 0.0000015496600045193012, - "sample": 98, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with allowDots", - "hz": 367165.535073916, - "rme": 0.5874425201490753, - "mean": 0.0000027235671774004737, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with comma arrays", - "hz": 618980.4478028473, - "rme": 1.0456187026653216, - "mean": 0.0000016155599155831688, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with depth limit", - "hz": 146680.99110890136, - "rme": 0.6964193009900447, - "mean": 0.000006817515974224384, - "sample": 95, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file diff --git a/benchmark/benchmark/results/benchmark-1750778914140.json b/benchmark/benchmark/results/benchmark-1750879162316.json similarity index 51% rename from benchmark/benchmark/results/benchmark-1750778914140.json rename to benchmark/benchmark/results/benchmark-1750879162316.json index 90b552b8..2d59a4ea 100644 --- a/benchmark/benchmark/results/benchmark-1750778914140.json +++ b/benchmark/benchmark/results/benchmark-1750879162316.json @@ -1,167 +1,167 @@ { - "timestamp": "2025-06-24T15:28:34.112Z", + "timestamp": "2025-06-25T19:19:22.296Z", "nodeVersion": "v20.10.0", "platform": "linux-x64", "results": [ { "name": "Parse simple tiny query", - "hz": 313154.9237376186, - "rme": 1.6903268709844506, - "mean": 0.0000031933076065502473, - "sample": 89, - "fastest": true, + "hz": 283712.1247363865, + "rme": 1.4340721443337705, + "mean": 0.0000035246995556822195, + "sample": 92, + "fastest": false, "slowest": false }, { "name": "Parse simple small query", - "hz": 156105.16490604475, - "rme": 1.4911410003488408, - "mean": 0.000006405937949598731, + "hz": 140374.58670602005, + "rme": 2.0393725760227475, + "mean": 0.000007123796575047115, "sample": 92, "fastest": false, "slowest": false }, { "name": "Parse simple medium query", - "hz": 38279.531183714214, - "rme": 1.3721360582304987, - "mean": 0.000026123621922136908, - "sample": 88, + "hz": 33798.74567111509, + "rme": 0.7919733299913311, + "mean": 0.00002958689679583627, + "sample": 90, "fastest": false, "slowest": false }, { "name": "Parse simple large query", - "hz": 7721.711765875682, - "rme": 1.4168845304510336, - "mean": 0.00012950496344855405, - "sample": 91, + "hz": 6894.306489756423, + "rme": 0.8556451072701352, + "mean": 0.00014504722142623082, + "sample": 93, "fastest": false, "slowest": true }, { "name": "Parse shallow nested", - "hz": 159240.97774527723, - "rme": 2.077739988722212, - "mean": 0.000006279790630271096, - "sample": 90, + "hz": 152027.90046354823, + "rme": 1.0757391285955522, + "mean": 0.0000065777399868767525, + "sample": 95, "fastest": false, "slowest": false }, { "name": "Parse medium nested", - "hz": 125325.57762735152, - "rme": 1.4522640415354382, - "mean": 0.000007979217163262898, - "sample": 89, + "hz": 110693.68244647182, + "rme": 1.6329413009913591, + "mean": 0.000009033939226690469, + "sample": 96, "fastest": false, "slowest": false }, { "name": "Parse deep nested", - "hz": 67652.28462050331, - "rme": 1.283961566034765, - "mean": 0.000014781466813863238, - "sample": 94, + "hz": 62915.45966541839, + "rme": 1.1603374298964768, + "mean": 0.000015894344654206698, + "sample": 89, "fastest": false, "slowest": false }, { "name": "Parse simple array", - "hz": 184863.47901255573, - "rme": 1.1110156469307348, - "mean": 0.000005409397277068885, - "sample": 92, + "hz": 171340.37613716777, + "rme": 1.0176742822438232, + "mean": 0.0000058363359678832665, + "sample": 94, "fastest": false, "slowest": false }, { "name": "Parse indexed array", - "hz": 109029.5236057738, - "rme": 1.346927383395607, - "mean": 0.000009171827656660913, - "sample": 96, + "hz": 97650.00694449448, + "rme": 1.4503870418314584, + "mean": 0.000010240654673669536, + "sample": 95, "fastest": false, "slowest": false }, { "name": "Parse mixed array/object", - "hz": 77122.65083054845, - "rme": 1.595987778716788, - "mean": 0.00001296635928914282, - "sample": 89, + "hz": 70641.89343143582, + "rme": 1.3272907612810165, + "mean": 0.00001415590595643629, + "sample": 91, "fastest": false, "slowest": false }, { "name": "Parse e-commerce query", - "hz": 54834.53920410963, - "rme": 1.3819655169571585, - "mean": 0.000018236681013726, - "sample": 91, + "hz": 49043.29457764863, + "rme": 0.9045123760955812, + "mean": 0.000020390147289487925, + "sample": 86, "fastest": false, "slowest": false }, { "name": "Parse form data", - "hz": 61005.265565852205, - "rme": 1.6642199569551437, - "mean": 0.000016392027650802517, - "sample": 88, + "hz": 57468.646530985556, + "rme": 1.114045273345342, + "mean": 0.000017400792612382594, + "sample": 90, "fastest": false, "slowest": false }, { "name": "Parse API query", - "hz": 49176.92558808578, - "rme": 1.4670771376986425, - "mean": 0.000020334740084733408, - "sample": 90, + "hz": 44861.25234335767, + "rme": 1.0558664755726865, + "mean": 0.000022290951495206394, + "sample": 94, "fastest": false, "slowest": false }, { "name": "Parse empty values", - "hz": 231208.43111982575, - "rme": 1.6106295431332698, - "mean": 0.00000432510179302995, + "hz": 211407.8559032721, + "rme": 1.3484339953526052, + "mean": 0.000004730193188551808, "sample": 92, "fastest": false, "slowest": false }, { "name": "Parse special characters", - "hz": 298818.6664572633, - "rme": 1.7054856848758984, - "mean": 0.0000033465111529202906, - "sample": 94, + "hz": 287350.7359591185, + "rme": 0.7552149644290912, + "mean": 0.000003480067648555702, + "sample": 93, "fastest": false, "slowest": false }, { "name": "Parse with allowDots", - "hz": 174176.8526101155, - "rme": 1.4217008872377315, - "mean": 0.000005741291021249766, - "sample": 93, + "hz": 172187.0462574967, + "rme": 1.1121055661099535, + "mean": 0.000005807637808622098, + "sample": 92, "fastest": false, "slowest": false }, { "name": "Parse with comma arrays", - "hz": 294126.4482630959, - "rme": 1.548132341106036, - "mean": 0.0000033998982611230546, - "sample": 92, - "fastest": false, + "hz": 297840.2200968278, + "rme": 0.5779512614194059, + "mean": 0.0000033575049053982714, + "sample": 98, + "fastest": true, "slowest": false }, { "name": "Parse with depth limit", - "hz": 73297.77982796784, - "rme": 1.217286057909573, - "mean": 0.000013642978032172747, - "sample": 92, + "hz": 70007.43549836402, + "rme": 0.7637508428985147, + "mean": 0.000014284196998236975, + "sample": 91, "fastest": false, "slowest": false } diff --git a/benchmark/fixtures.js b/benchmark/fixtures.js index 29c09f85..1636faf4 100644 --- a/benchmark/fixtures.js +++ b/benchmark/fixtures.js @@ -227,7 +227,8 @@ module.exports = { testObjects, // Helper functions - generateQuery: (type, size = 'medium') => { + generateQuery: (type, size) => { + size = size || 'medium'; const queries = { simple: simpleQueries, nested: nestedQueries, diff --git a/benchmark/package.json b/benchmark/package.json index 51e93469..1cd341be 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -3,8 +3,13 @@ "private": true, "description": "Benchmarking suite for qs library", "dependencies": { + "array-includes": "^3.1.9", + "array.from": "^1.1.6", "benchmark": "^2.1.4", - "colors": "=1.4.0" + "colors": "=1.4.0", + "es6-shim": "^0.35.8", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "string.prototype.repeat": "^1.0.0" } } - diff --git a/benchmark/polyfills.js b/benchmark/polyfills.js index 110c04a6..b1e805f5 100644 --- a/benchmark/polyfills.js +++ b/benchmark/polyfills.js @@ -2,130 +2,28 @@ /** * Polyfills for older Node.js versions (0.8+) - * This file provides implementations for features not available in older Node versions + * This file provides shims using well-maintained npm packages */ -// Polyfill for Array.from (ES6) -if (!Array.from) { - Array.from = function (arrayLike, mapFn, thisArg) { - var result = []; - var length, i; +// Load `Array.from` polyfill +require('array.from/auto'); - if (arrayLike == null) { - throw new TypeError('Array.from requires an array-like object'); - } - - length = arrayLike.length >>> 0; - - for (i = 0; i < length; i++) { - if (i in arrayLike) { - if (mapFn) { - result[i] = mapFn.call(thisArg, arrayLike[i], i, arrayLike); - } else { - result[i] = arrayLike[i]; - } - } - } - - return result; - }; -} - -// Polyfill for String.prototype.repeat (ES6) -if (!String.prototype.repeat) { - String.prototype.repeat = function (count) { - if (count < 0) { - throw new RangeError('repeat count must be non-negative'); - } - if (count === Infinity) { - throw new RangeError('repeat count must be less than infinity'); - } - - count = Math.floor(count); - if (this.length === 0 || count === 0) { - return ''; - } - - var result = ''; - var pattern = this; - - while (count > 0) { - if (count & 1) { - result += pattern; - } - count >>= 1; - pattern += pattern; - } - - return result; - }; -} - -// Polyfill for Object.fromEntries (ES2019) -if (!Object.fromEntries) { - Object.fromEntries = function (entries) { - var obj = {}; - - for (var i = 0; i < entries.length; i++) { - var entry = entries[i]; - if (Object.prototype.toString.call(entry) !== '[object Array]') { - throw new TypeError('Iterator value ' + entry + ' is not an entry object'); - } - obj[entry[0]] = entry[1]; - } +// Load `String.prototype.repeat` polyfill +require('string.prototype.repeat/auto'); - return obj; - }; -} +// Load `Object.entries` polyfill +require('object.entries/auto'); -// Polyfill for Object.entries (ES2017) -if (!Object.entries) { - Object.entries = function (obj) { - var ownProps = Object.keys(obj); - var i = ownProps.length; - var resArray = new Array(i); +// Load `Object.fromEntries` polyfill +require('object.fromentries/auto'); - while (i--) { - resArray[i] = [ownProps[i], obj[ownProps[i]]]; - } - - return resArray; - }; -} - -// Simplified polyfill for Promise (ES6) for Node.js 0.8+ -// For complex usage, consider using a full Promise library like Q or Bluebird -if (typeof Promise === 'undefined') { - global.Promise = function (executor) { - var callbacks = []; - var value; - var resolved = false; - - this.then = function (onFulfilled) { - if (resolved) { - onFulfilled(value); - } else { - callbacks.push(onFulfilled); - } - return this; - }; +// Load `Array.prototype.includes` polyfill +require('array-includes/auto'); - function resolve(val) { - value = val; - resolved = true; +// Load full ES6 shims (includes Promise and many more) +require('es6-shim'); - for (var i = 0; i < callbacks.length; i++) { - callbacks[i](value); - } - callbacks = []; - } - - executor(resolve); - }; -} - -// Ensure fs.mkdirSync recursive option works (Node.js 10+) -// This is a simple version that doesn't handle all edge cases +// Patch `fs.mkdirSync` to support recursive (for Node <10) var fs = require('fs'); var path = require('path'); @@ -154,34 +52,3 @@ fs.mkdirSync = function (dirPath, options) { return originalMkdirSync.apply(this, arguments); }; - -// Ensure Array includes method (ES7) -if (!Array.prototype.includes) { - Array.prototype.includes = function (searchElement, fromIndex) { - if (this == null) { - throw new TypeError('"this" is null or not defined'); - } - - var o = Object(this); - var len = o.length >>> 0; - - if (len === 0) { - return false; - } - - var n = fromIndex | 0; - var k = Math.max(n >= 0 ? n : len + n, 0); - - while (k < len) { - if (o[k] === searchElement) { - return true; - } - k++; - } - return false; - }; -} - -module.exports = { - loaded: true -}; diff --git a/benchmark/results/benchmark-1750775607814.json b/benchmark/results/benchmark-1750775607814.json deleted file mode 100644 index 9bda7aea..00000000 --- a/benchmark/results/benchmark-1750775607814.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "timestamp": "2025-06-24T14:33:27.757Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Parse simple tiny query", - "hz": 339368.57646296266, - "rme": 1.5877642145574582, - "mean": 0.000002946648774681518, - "sample": 93, - "fastest": true, - "slowest": false - }, - { - "name": "Parse simple small query", - "hz": 166303.78950936385, - "rme": 1.0841983813030203, - "mean": 0.000006013092082569137, - "sample": 89, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple medium query", - "hz": 38462.85790917391, - "rme": 1.4113066897602626, - "mean": 0.000025999108083996187, - "sample": 89, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple large query", - "hz": 8062.515205586793, - "rme": 1.057628921792131, - "mean": 0.00012403077383433221, - "sample": 93, - "fastest": false, - "slowest": true - }, - { - "name": "Parse shallow nested", - "hz": 169808.09843727067, - "rme": 0.8811370835416009, - "mean": 0.000005889000637795923, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Parse medium nested", - "hz": 132794.2803924927, - "rme": 0.6406034191302279, - "mean": 0.0000075304448131678215, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse deep nested", - "hz": 72472.98755695937, - "rme": 0.6823000589684686, - "mean": 0.00001379824447300535, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple array", - "hz": 193669.5397470639, - "rme": 0.5945380745133809, - "mean": 0.000005163434587111731, - "sample": 99, - "fastest": false, - "slowest": false - }, - { - "name": "Parse indexed array", - "hz": 111618.07377058547, - "rme": 1.389286676498194, - "mean": 0.00000895912253471918, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse mixed array/object", - "hz": 79964.95688603172, - "rme": 1.4153317502720602, - "mean": 0.000012505477886084874, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse e-commerce query", - "hz": 55869.88938147373, - "rme": 0.8232662184894164, - "mean": 0.00001789872883356015, - "sample": 86, - "fastest": false, - "slowest": false - }, - { - "name": "Parse form data", - "hz": 64758.34859642685, - "rme": 1.0831317727449514, - "mean": 0.000015442024413438744, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse API query", - "hz": 50796.577670369334, - "rme": 1.792982399893066, - "mean": 0.00001968636561481031, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Parse empty values", - "hz": 249045.98363261085, - "rme": 0.9984244240955832, - "mean": 0.000004015322734436007, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse special characters", - "hz": 326350.9418241071, - "rme": 0.8537521124373892, - "mean": 0.000003064186039760132, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with allowDots", - "hz": 188545.89300983382, - "rme": 0.9810731436902679, - "mean": 0.0000053037485146804226, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with comma arrays", - "hz": 324909.63376456907, - "rme": 0.6122238240837989, - "mean": 0.0000030777788531952375, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with depth limit", - "hz": 77198.2278999816, - "rme": 1.1472932485238967, - "mean": 0.000012953665222673309, - "sample": 98, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file diff --git a/benchmark/runner.js b/benchmark/runner.js index 0f43de64..76a143f5 100644 --- a/benchmark/runner.js +++ b/benchmark/runner.js @@ -9,15 +9,19 @@ var path = require('path'); require('./polyfills'); function table(data) { - return data.map(row => row.join('\t')).join('\n'); -}; + return data + .map(function (row) { + return row.join('\t'); + }) + .join('\n'); +} function BenchmarkRunner(options) { options = options || {}; this.options = { outputDir: options.outputDir || path.join(__dirname, 'results'), baseline: options.baseline || null, - verbose: options.verbose || false + verbose: options.verbose || false, }; // Copy any other options @@ -36,12 +40,12 @@ function BenchmarkRunner(options) { } } -BenchmarkRunner.prototype.add = function(name, fn, options) { +BenchmarkRunner.prototype.add = function (name, fn, options) { options = options || {}; var self = this; var completeOptions = { - onComplete: function(event) { + onComplete: function (event) { var benchmark = event.target; self.results.push({ name: benchmark.name, @@ -50,9 +54,9 @@ BenchmarkRunner.prototype.add = function(name, fn, options) { mean: benchmark.stats.mean, sample: benchmark.stats.sample.length, fastest: false, - slowest: false + slowest: false, }); - } + }, }; // Copy any other options @@ -66,42 +70,42 @@ BenchmarkRunner.prototype.add = function(name, fn, options) { return this; }; -BenchmarkRunner.prototype.run = function(callback) { +BenchmarkRunner.prototype.run = function (callback) { var self = this; // For backward compatibility, if no callback is provided, return a Promise if (!callback && typeof Promise !== 'undefined') { - return new Promise(function(resolve, reject) { - self._runWithCallbacks(function(err, results) { + return new Promise(function (resolve, reject) { + self._runWithCallbacks(function (err, results) { if (err) return reject(err); resolve(results); }); }); } - return this._runWithCallbacks(callback || function() {}); + return this._runWithCallbacks(callback || function () {}); }; -BenchmarkRunner.prototype._runWithCallbacks = function(callback) { +BenchmarkRunner.prototype._runWithCallbacks = function (callback) { var self = this; this.suite - .on('start', function() { + .on('start', function () { if (self.options.verbose) { console.log(colors.blue('πŸš€ Starting benchmarks...')); } }) - .on('cycle', function(event) { + .on('cycle', function (event) { if (self.options.verbose) { console.log(colors.gray(String(event.target))); } }) - .on('complete', function() { + .on('complete', function () { self._markFastestSlowest(); self._generateReport(); callback(null, self.results); }) - .on('error', function(err) { + .on('error', function (err) { callback(err); }) .run({ async: true }); @@ -109,17 +113,17 @@ BenchmarkRunner.prototype._runWithCallbacks = function(callback) { return this; }; -BenchmarkRunner.prototype._markFastestSlowest = function() { +BenchmarkRunner.prototype._markFastestSlowest = function () { if (this.results.length === 0) return; - var sorted = this.results.slice().sort(function(a, b) { + var sorted = this.results.slice().sort(function (a, b) { return b.hz - a.hz; }); sorted[0].fastest = true; sorted[sorted.length - 1].slowest = true; }; -BenchmarkRunner.prototype._generateReport = function() { +BenchmarkRunner.prototype._generateReport = function () { var timestamp = new Date().toISOString(); var nodeVersion = process.version; var platform = process.platform + '-' + process.arch; @@ -132,7 +136,7 @@ BenchmarkRunner.prototype._generateReport = function() { timestamp: timestamp, nodeVersion: nodeVersion, platform: platform, - results: this.results + results: this.results, }; var jsonPath = path.join(this.options.outputDir, 'benchmark-' + Date.now() + '.json'); @@ -144,33 +148,24 @@ BenchmarkRunner.prototype._generateReport = function() { } }; -BenchmarkRunner.prototype._printConsoleReport = function() { +BenchmarkRunner.prototype._printConsoleReport = function () { console.log(colors.green('\nπŸ“Š Benchmark Results\n')); - var tableData = [ - ['Test Name', 'Ops/sec', 'RME', 'Samples', 'Status'] - ]; + var tableData = [['Test Name', 'Ops/sec', 'RME', 'Samples', 'Status']]; for (var i = 0; i < this.results.length; i++) { var result = this.results[i]; var opsPerSec = result.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }); var rme = 'Β±' + result.rme.toFixed(2) + '%'; - var status = result.fastest ? 'πŸš€ Fastest' : - result.slowest ? '🐌 Slowest' : ''; - - tableData.push([ - result.name, - opsPerSec, - rme, - result.sample.toString(), - status - ]); + var status = result.fastest ? 'πŸš€ Fastest' : result.slowest ? '🐌 Slowest' : ''; + + tableData.push([result.name, opsPerSec, rme, result.sample.toString(), status]); } console.log(table(tableData)); }; -BenchmarkRunner.prototype._compareWithBaseline = function(currentReport) { +BenchmarkRunner.prototype._compareWithBaseline = function (currentReport) { if (!fs.existsSync(this.options.baseline)) { console.log(colors.yellow('⚠️ Baseline file not found, skipping comparison')); return; @@ -180,9 +175,7 @@ BenchmarkRunner.prototype._compareWithBaseline = function(currentReport) { var baseline = JSON.parse(fs.readFileSync(this.options.baseline, 'utf8')); console.log(colors.blue('\nπŸ“ˆ Performance Comparison\n')); - var comparisonData = [ - ['Test Name', 'Current', 'Baseline', 'Change', 'Status'] - ]; + var comparisonData = [['Test Name', 'Current', 'Baseline', 'Change', 'Status']]; for (var i = 0; i < currentReport.results.length; i++) { var current = currentReport.results[i]; @@ -197,28 +190,23 @@ BenchmarkRunner.prototype._compareWithBaseline = function(currentReport) { if (!baselineResult) continue; - var change = ((current.hz - baselineResult.hz) / baselineResult.hz * 100).toFixed(2); + var change = (((current.hz - baselineResult.hz) / baselineResult.hz) * 100).toFixed(2); var changeStr = (change >= 0 ? '+' : '') + change + '%'; - var status = Math.abs(change) < 5 ? 'βž– No change' : - change > 5 ? 'βœ… Improved' : - '⚠️ Regression'; + var status = Math.abs(change) < 5 ? 'βž– No change' : change > 5 ? 'βœ… Improved' : '⚠️ Regression'; comparisonData.push([ current.name, current.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }), baselineResult.hz.toLocaleString('en-US', { maximumFractionDigits: 0 }), changeStr, - status + status, ]); } console.log(table(comparisonData)); } catch (error) { - console.log(colors.red('❌ Error comparing with baseline: ' + error.message)); + console.error(colors.red('❌ Error comparing with baseline: ' + error.message)); } }; module.exports = BenchmarkRunner; - -module.exports = BenchmarkRunner; - diff --git a/benchmark/stringify.js b/benchmark/stringify.js index a5240dac..e4ec050b 100644 --- a/benchmark/stringify.js +++ b/benchmark/stringify.js @@ -2,7 +2,7 @@ var BenchmarkRunner = require('./runner'); var fixtures = require('./fixtures'); -var qs = require('../lib/index'); +var qs = require('../lib'); // Ensure polyfills are loaded require('./polyfills'); diff --git a/package.json b/package.json index 4d534f27..a0d7082e 100644 --- a/package.json +++ b/package.json @@ -77,12 +77,12 @@ "readme": "evalmd README.md", "postlint": "eclint check $(git ls-files | xargs find 2> /dev/null | grep -vE 'node_modules|\\.git' | grep -v dist/)", "lint": "eslint --ext=js,mjs .", - "benchmark": "cd benchmark && npm install && node index.js", - "benchmark:parse": "cd benchmark && npm install && node index.js --parse", - "benchmark:stringify": "cd benchmark && npm install && node index.js --stringify", - "benchmark:baseline": "cd benchmark && npm install && node index.js --save-baseline", - "benchmark:compare": "cd benchmark && npm install && node index.js --baseline benchmark/baseline.json", - "benchmark:stress": "cd benchmark && npm install && STRESS_TEST=1 node index.js", + "benchmark": "cd benchmark && npm install && node .", + "benchmark:parse": "cd benchmark && npm install && node . --parse", + "benchmark:stringify": "cd benchmark && npm install && node . --stringify", + "benchmark:baseline": "cd benchmark && npm install && node . --save-baseline", + "benchmark:compare": "cd benchmark && npm install && node . --baseline benchmark/baseline.json", + "benchmark:stress": "STRESS_TEST=1 npm run benchmark", "dist": "mkdirp dist && browserify --standalone Qs -g unassertify -g @browserify/envify -g [@browserify/uglifyify --mangle.keep_fnames --compress.keep_fnames --format.indent_level=1 --compress.arrows=false --compress.passes=4 --compress.typeofs=false] -p common-shakeify -p bundle-collapser/plugin lib/index.js > dist/qs.js" }, "license": "BSD-3-Clause", From 8135660589e1589bb4af57fe169ff8578503e3f0 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Wed, 25 Jun 2025 23:24:24 +0300 Subject: [PATCH 10/14] fix(benchmark): simplify benchmark script commands for improved readability --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a0d7082e..a7cf839a 100644 --- a/package.json +++ b/package.json @@ -78,10 +78,10 @@ "postlint": "eclint check $(git ls-files | xargs find 2> /dev/null | grep -vE 'node_modules|\\.git' | grep -v dist/)", "lint": "eslint --ext=js,mjs .", "benchmark": "cd benchmark && npm install && node .", - "benchmark:parse": "cd benchmark && npm install && node . --parse", - "benchmark:stringify": "cd benchmark && npm install && node . --stringify", - "benchmark:baseline": "cd benchmark && npm install && node . --save-baseline", - "benchmark:compare": "cd benchmark && npm install && node . --baseline benchmark/baseline.json", + "benchmark:parse": "npm run benchmark -- --parse", + "benchmark:stringify": "npm run benchmark -- --stringify", + "benchmark:baseline": "npm run benchmark -- --save-baseline", + "benchmark:compare": "npm run benchmark -- --baseline benchmark/baseline.json", "benchmark:stress": "STRESS_TEST=1 npm run benchmark", "dist": "mkdirp dist && browserify --standalone Qs -g unassertify -g @browserify/envify -g [@browserify/uglifyify --mangle.keep_fnames --compress.keep_fnames --format.indent_level=1 --compress.arrows=false --compress.passes=4 --compress.typeofs=false] -p common-shakeify -p bundle-collapser/plugin lib/index.js > dist/qs.js" }, From eb599f5801382eb7e971e2678e18ecf699e29e3b Mon Sep 17 00:00:00 2001 From: Mohamed Date: Wed, 25 Jun 2025 23:36:23 +0300 Subject: [PATCH 11/14] Fix formatting --- README.md | 155 ++++---- benchmark/baseline.json | 336 +++++++++--------- .../results/benchmark-1750879162316.json | 169 --------- benchmark/fixtures.js | 53 +-- benchmark/package.json | 26 +- 5 files changed, 300 insertions(+), 439 deletions(-) delete mode 100644 benchmark/benchmark/results/benchmark-1750879162316.json diff --git a/README.md b/README.md index 22c411df..6b17df8b 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ assert.equal(str, 'a=c'); ### Parsing Objects [](#preventEval) + ```javascript qs.parse(string, [options]); ``` @@ -44,8 +45,8 @@ For example, the string `'foo[bar]=baz'` converts to: ```javascript assert.deepEqual(qs.parse('foo[bar]=baz'), { foo: { - bar: 'baz' - } + bar: 'baz', + }, }); ``` @@ -57,7 +58,7 @@ assert.deepEqual(nullObject, { a: { hasOwnProperty: 'b' } }); ``` By default parameters that would overwrite properties on the object prototype are ignored, if you wish to keep the data from those fields either use `plainObjects` as mentioned above, or set `allowPrototypes` to `true` which will allow user input to overwrite those properties. -*WARNING* It is generally a bad idea to enable this option as it can cause problems when attempting to use the properties that have been overwritten. +_WARNING_ It is generally a bad idea to enable this option as it can cause problems when attempting to use the properties that have been overwritten. Always be careful with this option. ```javascript @@ -69,7 +70,7 @@ URI encoded strings work too: ```javascript assert.deepEqual(qs.parse('a%5Bb%5D=c'), { - a: { b: 'c' } + a: { b: 'c' }, }); ``` @@ -79,9 +80,9 @@ You can also nest your objects, like `'foo[bar][baz]=foobarbaz'`: assert.deepEqual(qs.parse('foo[bar][baz]=foobarbaz'), { foo: { bar: { - baz: 'foobarbaz' - } - } + baz: 'foobarbaz', + }, + }, }); ``` @@ -96,13 +97,13 @@ var expected = { d: { e: { f: { - '[g][h][i]': 'j' - } - } - } - } - } - } + '[g][h][i]': 'j', + }, + }, + }, + }, + }, + }, }; var string = 'a[b][c][d][e][f][g][h][i]=j'; assert.deepEqual(qs.parse(string), expected); @@ -136,6 +137,7 @@ assert.deepEqual(limited, { a: 'b' }); ``` If you want an error to be thrown whenever the a limit is exceeded (eg, `parameterLimit`, `arrayLimit`), set the `throwOnLimitExceeded` option to `true`. This option will generate a descriptive error if the query string exceeds a configured limit. + ```javascript try { qs.parse('a=1&b=2&c=3&d=4', { parameterLimit: 3, throwOnLimitExceeded: true }); @@ -180,16 +182,18 @@ Note: it implies `allowDots`, so `parse` will error if you set `decodeDotInKeys` ```javascript var withDots = qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { decodeDotInKeys: true }); -assert.deepEqual(withDots, { 'name.obj': { first: 'John', last: 'Doe' }}); +assert.deepEqual(withDots, { 'name.obj': { first: 'John', last: 'Doe' } }); ``` Option `allowEmptyArrays` can be used to allowing empty array values in object + ```javascript var withEmptyArrays = qs.parse('foo[]&bar=baz', { allowEmptyArrays: true }); assert.deepEqual(withEmptyArrays, { foo: [], bar: 'baz' }); ``` Option `duplicates` can be used to change the behavior when duplicate keys are encountered + ```javascript assert.deepEqual(qs.parse('foo=bar&foo=baz'), { foo: ['bar', 'baz'] }); assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'combine' }), { foo: ['bar', 'baz'] }); @@ -205,7 +209,7 @@ assert.deepEqual(oldCharset, { a: 'Β§' }); ``` Some services add an initial `utf8=βœ“` value to forms so that old Internet Explorer versions are more likely to submit the form as utf-8. -Additionally, the server can check the value against wrong encodings of the checkmark character and detect that a query string or `application/x-www-form-urlencoded` body was *not* sent as utf-8, eg. if the form had an `accept-charset` parameter or the containing page had a different character set. +Additionally, the server can check the value against wrong encodings of the checkmark character and detect that a query string or `application/x-www-form-urlencoded` body was _not_ sent as utf-8, eg. if the form had an `accept-charset` parameter or the containing page had a different character set. **qs** supports this mechanism via the `charsetSentinel` option. If specified, the `utf8` parameter will be omitted from the returned object. @@ -217,14 +221,14 @@ In that sense the `charset` will behave as the default charset rather than the a ```javascript var detectedAsUtf8 = qs.parse('utf8=%E2%9C%93&a=%C3%B8', { charset: 'iso-8859-1', - charsetSentinel: true + charsetSentinel: true, }); assert.deepEqual(detectedAsUtf8, { a: 'ΓΈ' }); // Browsers encode the checkmark as ✓ when submitting as iso-8859-1: var detectedAsIso8859_1 = qs.parse('utf8=%26%2310003%3B&a=%F8', { charset: 'utf-8', - charsetSentinel: true + charsetSentinel: true, }); assert.deepEqual(detectedAsIso8859_1, { a: 'ΓΈ' }); ``` @@ -234,7 +238,7 @@ If you want to decode the `&#...;` syntax to the actual character, you can speci ```javascript var detectedAsIso8859_1 = qs.parse('a=%26%239786%3B', { charset: 'iso-8859-1', - interpretNumericEntities: true + interpretNumericEntities: true, }); assert.deepEqual(detectedAsIso8859_1, { a: '☺' }); ``` @@ -288,17 +292,18 @@ This is needed to handle cases when someone sent, for example, `a[999999999]` an ```javascript var withMaxIndex = qs.parse('a[100]=b'); -assert.deepEqual(withMaxIndex, { a: { '100': 'b' } }); +assert.deepEqual(withMaxIndex, { a: { 100: 'b' } }); ``` This limit can be overridden by passing an `arrayLimit` option: ```javascript var withArrayLimit = qs.parse('a[1]=b', { arrayLimit: 0 }); -assert.deepEqual(withArrayLimit, { a: { '1': 'b' } }); +assert.deepEqual(withArrayLimit, { a: { 1: 'b' } }); ``` If you want to throw an error whenever the array limit is exceeded, set the `throwOnLimitExceeded` option to `true`. This option will generate a descriptive error if the query string exceeds a configured limit. + ```javascript try { qs.parse('a[1]=b', { arrayLimit: 0, throwOnLimitExceeded: true }); @@ -314,14 +319,14 @@ To disable array parsing entirely, set `parseArrays` to `false`. ```javascript var noParsingArrays = qs.parse('a[]=b', { parseArrays: false }); -assert.deepEqual(noParsingArrays, { a: { '0': 'b' } }); +assert.deepEqual(noParsingArrays, { a: { 0: 'b' } }); ``` If you mix notations, **qs** will merge the two items into an object: ```javascript var mixedNotation = qs.parse('a[0]=b&a[b]=c'); -assert.deepEqual(mixedNotation, { a: { '0': 'b', b: 'c' } }); +assert.deepEqual(mixedNotation, { a: { 0: 'b', b: 'c' } }); ``` You can also create arrays of objects: @@ -332,10 +337,12 @@ assert.deepEqual(arraysOfObjects, { a: [{ b: 'c' }] }); ``` Some people use comma to join array, **qs** can parse it: + ```javascript -var arraysOfObjects = qs.parse('a=b,c', { comma: true }) -assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] }) +var arraysOfObjects = qs.parse('a=b,c', { comma: true }); +assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] }); ``` + (_this cannot convert nested objects, such as `a={b:1},{c:d}`_) ### Parsing primitive/scalar values (numbers, booleans, null, etc) @@ -353,6 +360,7 @@ If you wish to auto-convert values which look like numbers, booleans, and other ### Stringifying [](#preventEval) + ```javascript qs.stringify(object, [options]); ``` @@ -372,21 +380,24 @@ assert.equal(unencoded, 'a[b]=c'); ``` Encoding can be disabled for keys by setting the `encodeValuesOnly` option to `true`: + ```javascript -var encodedValues = qs.stringify( - { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, - { encodeValuesOnly: true } -); -assert.equal(encodedValues,'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'); +var encodedValues = qs.stringify({ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, { encodeValuesOnly: true }); +assert.equal(encodedValues, 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'); ``` This encoding can also be replaced by a custom encoding method set as `encoder` option: ```javascript -var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str) { - // Passed in values `a`, `b`, `c` - return // Return encoded string -}}) +var encoded = qs.stringify( + { a: { b: 'c' } }, + { + encoder: function (str) { + // Passed in values `a`, `b`, `c` + return; // Return encoded string + }, + } +); ``` _(Note: the `encoder` option does not apply if `encode` is `false`)_ @@ -394,38 +405,47 @@ _(Note: the `encoder` option does not apply if `encode` is `false`)_ Analogue to the `encoder` there is a `decoder` option for `parse` to override decoding of properties and values: ```javascript -var decoded = qs.parse('x=z', { decoder: function (str) { - // Passed in values `x`, `z` - return // Return decoded string -}}) +var decoded = qs.parse('x=z', { + decoder: function (str) { + // Passed in values `x`, `z` + return; // Return decoded string + }, +}); ``` You can encode keys and values using different logic by using the type argument provided to the encoder: ```javascript -var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str, defaultEncoder, charset, type) { - if (type === 'key') { - return // Encoded key - } else if (type === 'value') { - return // Encoded value +var encoded = qs.stringify( + { a: { b: 'c' } }, + { + encoder: function (str, defaultEncoder, charset, type) { + if (type === 'key') { + return; // Encoded key + } else if (type === 'value') { + return; // Encoded value + } + }, } -}}) +); ``` The type argument is also provided to the decoder: ```javascript -var decoded = qs.parse('x=z', { decoder: function (str, defaultDecoder, charset, type) { - if (type === 'key') { - return // Decoded key - } else if (type === 'value') { - return // Decoded value - } -}}) +var decoded = qs.parse('x=z', { + decoder: function (str, defaultDecoder, charset, type) { + if (type === 'key') { + return; // Decoded key + } else if (type === 'value') { + return; // Decoded value + } + }, +}); ``` Examples beyond this point will be shown as though the output is not URI encoded for clarity. -Please note that the return values in these cases *will* be URI encoded during real usage. +Please note that the return values in these cases _will_ be URI encoded during real usage. When arrays are stringified, they follow the `arrayFormat` option, which defaults to `indices`: @@ -444,13 +464,13 @@ qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }); You may use the `arrayFormat` option to specify the format of the output array: ```javascript -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }) +qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }); // 'a[0]=b&a[1]=c' -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }) +qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }); // 'a[]=b&a[]=c' -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }) +qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }); // 'a=b&a=c' -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' }) +qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' }); // 'a=b,c' ``` @@ -473,12 +493,14 @@ qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true }); You may encode the dot notation in the keys of object with option `encodeDotInKeys` by setting it to `true`: Note: it implies `allowDots`, so `stringify` will error if you set `decodeDotInKeys` to `true`, and `allowDots` to `false`. Caveat: when `encodeValuesOnly` is `true` as well as `encodeDotInKeys`, only dots in keys and nothing else will be encoded. + ```javascript -qs.stringify({ "name.obj": { "first": "John", "last": "Doe" } }, { allowDots: true, encodeDotInKeys: true }) +qs.stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: true }); // 'name%252Eobj.first=John&name%252Eobj.last=Doe' ``` You may allow empty array values by setting the `allowEmptyArrays` option to `true`: + ```javascript qs.stringify({ foo: [], bar: 'baz' }, { allowEmptyArrays: true }); // 'foo[]&bar=baz' @@ -496,8 +518,8 @@ Key with no values (such as an empty object or array) will return nothing: assert.equal(qs.stringify({ a: [] }), ''); assert.equal(qs.stringify({ a: {} }), ''); assert.equal(qs.stringify({ a: [{}] }), ''); -assert.equal(qs.stringify({ a: { b: []} }), ''); -assert.equal(qs.stringify({ a: { b: {}} }), ''); +assert.equal(qs.stringify({ a: { b: [] } }), ''); +assert.equal(qs.stringify({ a: { b: {} } }), ''); ``` Properties that are set to `undefined` will be omitted entirely: @@ -524,7 +546,14 @@ If you only want to override the serialization of `Date` objects, you can provid var date = new Date(7); assert.equal(qs.stringify({ a: date }), 'a=1970-01-01T00:00:00.007Z'.replace(/:/g, '%3A')); assert.equal( - qs.stringify({ a: date }, { serializeDate: function (d) { return d.getTime(); } }), + qs.stringify( + { a: date }, + { + serializeDate: function (d) { + return d.getTime(); + }, + } + ), 'a=7' ); ``` @@ -535,7 +564,7 @@ You may use the `sort` option to affect the order of parameter keys: function alphabeticalSort(a, b) { return a.localeCompare(b); } -assert.equal(qs.stringify({ a: 'c', z: 'y', b : 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y'); +assert.equal(qs.stringify({ a: 'c', z: 'y', b: 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y'); ``` Finally, you can use the `filter` option to restrict which keys will be included in the stringified output. @@ -637,7 +666,7 @@ assert.deepEqual(parsedStrictNull, { a: null, b: '' }); To completely skip rendering keys with `null` values, use the `skipNulls` flag: ```javascript -var nullsSkipped = qs.stringify({ a: 'b', c: null}, { skipNulls: true }); +var nullsSkipped = qs.stringify({ a: 'b', c: null }, { skipNulls: true }); assert.equal(nullsSkipped, 'a=b'); ``` @@ -689,7 +718,7 @@ assert.deepEqual(obj, { a: 'こんにけは!' }); ### RFC 3986 and RFC 1738 space encoding -RFC3986 used as default option and encodes ' ' to *%20* which is backward compatible. +RFC3986 used as default option and encodes ' ' to _%20_ which is backward compatible. In the same time, output can be stringified as per RFC1738 with ' ' equal to '+'. ``` diff --git a/benchmark/baseline.json b/benchmark/baseline.json index 9bda7aea..a9396ffb 100644 --- a/benchmark/baseline.json +++ b/benchmark/baseline.json @@ -1,169 +1,169 @@ { - "timestamp": "2025-06-24T14:33:27.757Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Parse simple tiny query", - "hz": 339368.57646296266, - "rme": 1.5877642145574582, - "mean": 0.000002946648774681518, - "sample": 93, - "fastest": true, - "slowest": false - }, - { - "name": "Parse simple small query", - "hz": 166303.78950936385, - "rme": 1.0841983813030203, - "mean": 0.000006013092082569137, - "sample": 89, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple medium query", - "hz": 38462.85790917391, - "rme": 1.4113066897602626, - "mean": 0.000025999108083996187, - "sample": 89, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple large query", - "hz": 8062.515205586793, - "rme": 1.057628921792131, - "mean": 0.00012403077383433221, - "sample": 93, - "fastest": false, - "slowest": true - }, - { - "name": "Parse shallow nested", - "hz": 169808.09843727067, - "rme": 0.8811370835416009, - "mean": 0.000005889000637795923, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Parse medium nested", - "hz": 132794.2803924927, - "rme": 0.6406034191302279, - "mean": 0.0000075304448131678215, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse deep nested", - "hz": 72472.98755695937, - "rme": 0.6823000589684686, - "mean": 0.00001379824447300535, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple array", - "hz": 193669.5397470639, - "rme": 0.5945380745133809, - "mean": 0.000005163434587111731, - "sample": 99, - "fastest": false, - "slowest": false - }, - { - "name": "Parse indexed array", - "hz": 111618.07377058547, - "rme": 1.389286676498194, - "mean": 0.00000895912253471918, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse mixed array/object", - "hz": 79964.95688603172, - "rme": 1.4153317502720602, - "mean": 0.000012505477886084874, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse e-commerce query", - "hz": 55869.88938147373, - "rme": 0.8232662184894164, - "mean": 0.00001789872883356015, - "sample": 86, - "fastest": false, - "slowest": false - }, - { - "name": "Parse form data", - "hz": 64758.34859642685, - "rme": 1.0831317727449514, - "mean": 0.000015442024413438744, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse API query", - "hz": 50796.577670369334, - "rme": 1.792982399893066, - "mean": 0.00001968636561481031, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Parse empty values", - "hz": 249045.98363261085, - "rme": 0.9984244240955832, - "mean": 0.000004015322734436007, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse special characters", - "hz": 326350.9418241071, - "rme": 0.8537521124373892, - "mean": 0.000003064186039760132, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with allowDots", - "hz": 188545.89300983382, - "rme": 0.9810731436902679, - "mean": 0.0000053037485146804226, - "sample": 97, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with comma arrays", - "hz": 324909.63376456907, - "rme": 0.6122238240837989, - "mean": 0.0000030777788531952375, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with depth limit", - "hz": 77198.2278999816, - "rme": 1.1472932485238967, - "mean": 0.000012953665222673309, - "sample": 98, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file + "timestamp": "2025-06-24T14:33:27.757Z", + "nodeVersion": "v20.10.0", + "platform": "linux-x64", + "results": [ + { + "name": "Parse simple tiny query", + "hz": 339368.57646296266, + "rme": 1.5877642145574582, + "mean": 0.000002946648774681518, + "sample": 93, + "fastest": true, + "slowest": false + }, + { + "name": "Parse simple small query", + "hz": 166303.78950936385, + "rme": 1.0841983813030203, + "mean": 0.000006013092082569137, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple medium query", + "hz": 38462.85790917391, + "rme": 1.4113066897602626, + "mean": 0.000025999108083996187, + "sample": 89, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple large query", + "hz": 8062.515205586793, + "rme": 1.057628921792131, + "mean": 0.00012403077383433221, + "sample": 93, + "fastest": false, + "slowest": true + }, + { + "name": "Parse shallow nested", + "hz": 169808.09843727067, + "rme": 0.8811370835416009, + "mean": 0.000005889000637795923, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse medium nested", + "hz": 132794.2803924927, + "rme": 0.6406034191302279, + "mean": 0.0000075304448131678215, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse deep nested", + "hz": 72472.98755695937, + "rme": 0.6823000589684686, + "mean": 0.00001379824447300535, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse simple array", + "hz": 193669.5397470639, + "rme": 0.5945380745133809, + "mean": 0.000005163434587111731, + "sample": 99, + "fastest": false, + "slowest": false + }, + { + "name": "Parse indexed array", + "hz": 111618.07377058547, + "rme": 1.389286676498194, + "mean": 0.00000895912253471918, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse mixed array/object", + "hz": 79964.95688603172, + "rme": 1.4153317502720602, + "mean": 0.000012505477886084874, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse e-commerce query", + "hz": 55869.88938147373, + "rme": 0.8232662184894164, + "mean": 0.00001789872883356015, + "sample": 86, + "fastest": false, + "slowest": false + }, + { + "name": "Parse form data", + "hz": 64758.34859642685, + "rme": 1.0831317727449514, + "mean": 0.000015442024413438744, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse API query", + "hz": 50796.577670369334, + "rme": 1.792982399893066, + "mean": 0.00001968636561481031, + "sample": 95, + "fastest": false, + "slowest": false + }, + { + "name": "Parse empty values", + "hz": 249045.98363261085, + "rme": 0.9984244240955832, + "mean": 0.000004015322734436007, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse special characters", + "hz": 326350.9418241071, + "rme": 0.8537521124373892, + "mean": 0.000003064186039760132, + "sample": 92, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with allowDots", + "hz": 188545.89300983382, + "rme": 0.9810731436902679, + "mean": 0.0000053037485146804226, + "sample": 97, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with comma arrays", + "hz": 324909.63376456907, + "rme": 0.6122238240837989, + "mean": 0.0000030777788531952375, + "sample": 93, + "fastest": false, + "slowest": false + }, + { + "name": "Parse with depth limit", + "hz": 77198.2278999816, + "rme": 1.1472932485238967, + "mean": 0.000012953665222673309, + "sample": 98, + "fastest": false, + "slowest": false + } + ] +} diff --git a/benchmark/benchmark/results/benchmark-1750879162316.json b/benchmark/benchmark/results/benchmark-1750879162316.json deleted file mode 100644 index 2d59a4ea..00000000 --- a/benchmark/benchmark/results/benchmark-1750879162316.json +++ /dev/null @@ -1,169 +0,0 @@ -{ - "timestamp": "2025-06-25T19:19:22.296Z", - "nodeVersion": "v20.10.0", - "platform": "linux-x64", - "results": [ - { - "name": "Parse simple tiny query", - "hz": 283712.1247363865, - "rme": 1.4340721443337705, - "mean": 0.0000035246995556822195, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple small query", - "hz": 140374.58670602005, - "rme": 2.0393725760227475, - "mean": 0.000007123796575047115, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple medium query", - "hz": 33798.74567111509, - "rme": 0.7919733299913311, - "mean": 0.00002958689679583627, - "sample": 90, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple large query", - "hz": 6894.306489756423, - "rme": 0.8556451072701352, - "mean": 0.00014504722142623082, - "sample": 93, - "fastest": false, - "slowest": true - }, - { - "name": "Parse shallow nested", - "hz": 152027.90046354823, - "rme": 1.0757391285955522, - "mean": 0.0000065777399868767525, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Parse medium nested", - "hz": 110693.68244647182, - "rme": 1.6329413009913591, - "mean": 0.000009033939226690469, - "sample": 96, - "fastest": false, - "slowest": false - }, - { - "name": "Parse deep nested", - "hz": 62915.45966541839, - "rme": 1.1603374298964768, - "mean": 0.000015894344654206698, - "sample": 89, - "fastest": false, - "slowest": false - }, - { - "name": "Parse simple array", - "hz": 171340.37613716777, - "rme": 1.0176742822438232, - "mean": 0.0000058363359678832665, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Parse indexed array", - "hz": 97650.00694449448, - "rme": 1.4503870418314584, - "mean": 0.000010240654673669536, - "sample": 95, - "fastest": false, - "slowest": false - }, - { - "name": "Parse mixed array/object", - "hz": 70641.89343143582, - "rme": 1.3272907612810165, - "mean": 0.00001415590595643629, - "sample": 91, - "fastest": false, - "slowest": false - }, - { - "name": "Parse e-commerce query", - "hz": 49043.29457764863, - "rme": 0.9045123760955812, - "mean": 0.000020390147289487925, - "sample": 86, - "fastest": false, - "slowest": false - }, - { - "name": "Parse form data", - "hz": 57468.646530985556, - "rme": 1.114045273345342, - "mean": 0.000017400792612382594, - "sample": 90, - "fastest": false, - "slowest": false - }, - { - "name": "Parse API query", - "hz": 44861.25234335767, - "rme": 1.0558664755726865, - "mean": 0.000022290951495206394, - "sample": 94, - "fastest": false, - "slowest": false - }, - { - "name": "Parse empty values", - "hz": 211407.8559032721, - "rme": 1.3484339953526052, - "mean": 0.000004730193188551808, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse special characters", - "hz": 287350.7359591185, - "rme": 0.7552149644290912, - "mean": 0.000003480067648555702, - "sample": 93, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with allowDots", - "hz": 172187.0462574967, - "rme": 1.1121055661099535, - "mean": 0.000005807637808622098, - "sample": 92, - "fastest": false, - "slowest": false - }, - { - "name": "Parse with comma arrays", - "hz": 297840.2200968278, - "rme": 0.5779512614194059, - "mean": 0.0000033575049053982714, - "sample": 98, - "fastest": true, - "slowest": false - }, - { - "name": "Parse with depth limit", - "hz": 70007.43549836402, - "rme": 0.7637508428985147, - "mean": 0.000014284196998236975, - "sample": 91, - "fastest": false, - "slowest": false - } - ] -} \ No newline at end of file diff --git a/benchmark/fixtures.js b/benchmark/fixtures.js index 1636faf4..0336da31 100644 --- a/benchmark/fixtures.js +++ b/benchmark/fixtures.js @@ -33,7 +33,7 @@ function repeatString(str, count) { // Simple flat query strings var simpleQueries = { tiny: 'a=1&b=2&c=3', - small: 'name=John&age=30&city=NYC&country=USA&email=john@example.com' + small: 'name=John&age=30&city=NYC&country=USA&email=john@example.com', }; // Generate medium and large query strings using a loop @@ -60,7 +60,7 @@ var nestedQueries = { shallow: 'user[name]=John&user[age]=30&user[email]=john@example.com', medium: 'user[profile][name]=John&user[profile][bio]=Developer&user[settings][theme]=dark', deep: 'user[profile][personal][name][first]=John&user[profile][personal][name][last]=Doe&user[profile][personal][address][street]=123 Main St&user[profile][personal][address][city]=NYC', - veryDeep: 'a[b][c][d][e][f][g][h][i][j]=deep' + veryDeep: 'a[b][c][d][e][f][g][h][i][j]=deep', }; // Array queries @@ -68,7 +68,7 @@ var arrayQueries = { simple: 'colors[]=red&colors[]=green&colors[]=blue', indexed: 'items[0]=first&items[1]=second&items[2]=third&items[3]=fourth&items[4]=fifth', sparse: 'sparse[1]=second&sparse[5]=sixth&sparse[10]=tenth', - mixed: 'user[name]=John&user[hobbies][]=reading&user[hobbies][]=coding&user[addresses][0][street]=123 Main&user[addresses][0][city]=NYC' + mixed: 'user[name]=John&user[hobbies][]=reading&user[hobbies][]=coding&user[addresses][0][street]=123 Main&user[addresses][0][city]=NYC', }; // Generate large array query string @@ -81,7 +81,7 @@ for (var i = 0; i < 50; i++) { // Edge case queries var edgeCaseQueries = { emptyValues: 'a=&b=&c=value&d=', - specialChars: 'special=%21%40%23%24%25%5E%26%2A%28%29&unicode=%E2%9C%93' + specialChars: 'special=%21%40%23%24%25%5E%26%2A%28%29&unicode=%E2%9C%93', }; // Generate long values - repeat 'x' 1000 times @@ -104,15 +104,17 @@ edgeCaseQueries.deepNesting += '=value'; // Mixed complexity queries (realistic scenarios) var realWorldQueries = { - ecommerce: 'category=electronics&subcategory=phones&brand[]=apple&brand[]=samsung&price[min]=100&price[max]=1000&features[]=camera&features[]=5g&sort=price&order=asc&page=2&limit=20', + ecommerce: + 'category=electronics&subcategory=phones&brand[]=apple&brand[]=samsung&price[min]=100&price[max]=1000&features[]=camera&features[]=5g&sort=price&order=asc&page=2&limit=20', form: 'user[name]=John%20Doe&user[email]=john%40example.com&user[age]=30&preferences[newsletter]=true&preferences[theme]=dark&address[street]=123%20Main%20St&address[city]=New%20York&address[zip]=10001', - analytics: 'event=pageview×tamp=1640995200&user[id]=12345&user[session]=abc123&page[url]=/products/123&page[title]=Product%20Details&utm[source]=google&utm[medium]=cpc&utm[campaign]=summer_sale', - api: 'fields[]=id&fields[]=name&fields[]=email&include[]=profile&include[]=settings&filter[status]=active&filter[created_after]=2023-01-01&sort[]=name&sort[]=-created_at&page[number]=1&page[size]=25' + analytics: + 'event=pageview×tamp=1640995200&user[id]=12345&user[session]=abc123&page[url]=/products/123&page[title]=Product%20Details&utm[source]=google&utm[medium]=cpc&utm[campaign]=summer_sale', + api: 'fields[]=id&fields[]=name&fields[]=email&include[]=profile&include[]=settings&filter[status]=active&filter[created_after]=2023-01-01&sort[]=name&sort[]=-created_at&page[number]=1&page[size]=25', }; // Performance stress tests var stressTestQueries = { - deepNesting: 'a' + repeatString('[level]', 20) + '=deep' + deepNesting: 'a' + repeatString('[level]', 20) + '=deep', }; // Generate manyKeys @@ -153,13 +155,13 @@ var testObjects = { name: 'John', profile: { age: 30, - bio: 'Developer' - } - } + bio: 'Developer', + }, + }, }, arrays: { colors: ['red', 'green', 'blue'], - numbers: [1, 2, 3, 4, 5] + numbers: [1, 2, 3, 4, 5], }, mixed: { name: 'John', @@ -169,17 +171,17 @@ var testObjects = { city: 'NYC', coordinates: { lat: 40.7128, - lng: -74.0060 - } + lng: -74.006, + }, }, preferences: { theme: 'dark', notifications: { email: true, - push: false - } - } - } + push: false, + }, + }, + }, }; // Generate large object without using Object.fromEntries @@ -198,7 +200,7 @@ module.exports = { testObjects: testObjects, // Helper functions - generateQuery: function(type, size) { + generateQuery: function (type, size) { size = size || 'medium'; var queries = { simple: simpleQueries, @@ -206,15 +208,15 @@ module.exports = { array: arrayQueries, edge: edgeCaseQueries, real: realWorldQueries, - stress: stressTestQueries + stress: stressTestQueries, }; return queries[type] && queries[type][size] ? queries[type][size] : simpleQueries.medium; }, - generateObject: function(type) { + generateObject: function (type) { return testObjects[type] || testObjects.simple; - } + }, }; module.exports = { @@ -235,14 +237,13 @@ module.exports = { array: arrayQueries, edge: edgeCaseQueries, real: realWorldQueries, - stress: stressTestQueries + stress: stressTestQueries, }; - return (queries[type] && queries[type][size]) ? queries[type][size] : simpleQueries.medium; + return queries[type] && queries[type][size] ? queries[type][size] : simpleQueries.medium; }, generateObject: (type) => { return testObjects[type] || testObjects.simple; - } + }, }; - diff --git a/benchmark/package.json b/benchmark/package.json index 1cd341be..ce59ed3f 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -1,15 +1,15 @@ { - "name": "qs-benchmarks", - "private": true, - "description": "Benchmarking suite for qs library", - "dependencies": { - "array-includes": "^3.1.9", - "array.from": "^1.1.6", - "benchmark": "^2.1.4", - "colors": "=1.4.0", - "es6-shim": "^0.35.8", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "string.prototype.repeat": "^1.0.0" - } + "name": "qs-benchmarks", + "private": true, + "description": "Benchmarking suite for qs library", + "dependencies": { + "array-includes": "^3.1.9", + "array.from": "^1.1.6", + "benchmark": "^2.1.4", + "colors": "=1.4.0", + "es6-shim": "^0.35.8", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "string.prototype.repeat": "^1.0.0" + } } From 11ab608003cd4cb2ec5ec99e7d04faf9f1584fbd Mon Sep 17 00:00:00 2001 From: Mohamed Date: Thu, 26 Jun 2025 00:17:14 +0300 Subject: [PATCH 12/14] Revert "Fix formatting" This reverts commit eb599f5801382eb7e971e2678e18ecf699e29e3b. --- README.md | 155 ++++++++++++++++++++++-------------------------------- 1 file changed, 63 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 6b17df8b..22c411df 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ assert.equal(str, 'a=c'); ### Parsing Objects [](#preventEval) - ```javascript qs.parse(string, [options]); ``` @@ -45,8 +44,8 @@ For example, the string `'foo[bar]=baz'` converts to: ```javascript assert.deepEqual(qs.parse('foo[bar]=baz'), { foo: { - bar: 'baz', - }, + bar: 'baz' + } }); ``` @@ -58,7 +57,7 @@ assert.deepEqual(nullObject, { a: { hasOwnProperty: 'b' } }); ``` By default parameters that would overwrite properties on the object prototype are ignored, if you wish to keep the data from those fields either use `plainObjects` as mentioned above, or set `allowPrototypes` to `true` which will allow user input to overwrite those properties. -_WARNING_ It is generally a bad idea to enable this option as it can cause problems when attempting to use the properties that have been overwritten. +*WARNING* It is generally a bad idea to enable this option as it can cause problems when attempting to use the properties that have been overwritten. Always be careful with this option. ```javascript @@ -70,7 +69,7 @@ URI encoded strings work too: ```javascript assert.deepEqual(qs.parse('a%5Bb%5D=c'), { - a: { b: 'c' }, + a: { b: 'c' } }); ``` @@ -80,9 +79,9 @@ You can also nest your objects, like `'foo[bar][baz]=foobarbaz'`: assert.deepEqual(qs.parse('foo[bar][baz]=foobarbaz'), { foo: { bar: { - baz: 'foobarbaz', - }, - }, + baz: 'foobarbaz' + } + } }); ``` @@ -97,13 +96,13 @@ var expected = { d: { e: { f: { - '[g][h][i]': 'j', - }, - }, - }, - }, - }, - }, + '[g][h][i]': 'j' + } + } + } + } + } + } }; var string = 'a[b][c][d][e][f][g][h][i]=j'; assert.deepEqual(qs.parse(string), expected); @@ -137,7 +136,6 @@ assert.deepEqual(limited, { a: 'b' }); ``` If you want an error to be thrown whenever the a limit is exceeded (eg, `parameterLimit`, `arrayLimit`), set the `throwOnLimitExceeded` option to `true`. This option will generate a descriptive error if the query string exceeds a configured limit. - ```javascript try { qs.parse('a=1&b=2&c=3&d=4', { parameterLimit: 3, throwOnLimitExceeded: true }); @@ -182,18 +180,16 @@ Note: it implies `allowDots`, so `parse` will error if you set `decodeDotInKeys` ```javascript var withDots = qs.parse('name%252Eobj.first=John&name%252Eobj.last=Doe', { decodeDotInKeys: true }); -assert.deepEqual(withDots, { 'name.obj': { first: 'John', last: 'Doe' } }); +assert.deepEqual(withDots, { 'name.obj': { first: 'John', last: 'Doe' }}); ``` Option `allowEmptyArrays` can be used to allowing empty array values in object - ```javascript var withEmptyArrays = qs.parse('foo[]&bar=baz', { allowEmptyArrays: true }); assert.deepEqual(withEmptyArrays, { foo: [], bar: 'baz' }); ``` Option `duplicates` can be used to change the behavior when duplicate keys are encountered - ```javascript assert.deepEqual(qs.parse('foo=bar&foo=baz'), { foo: ['bar', 'baz'] }); assert.deepEqual(qs.parse('foo=bar&foo=baz', { duplicates: 'combine' }), { foo: ['bar', 'baz'] }); @@ -209,7 +205,7 @@ assert.deepEqual(oldCharset, { a: 'Β§' }); ``` Some services add an initial `utf8=βœ“` value to forms so that old Internet Explorer versions are more likely to submit the form as utf-8. -Additionally, the server can check the value against wrong encodings of the checkmark character and detect that a query string or `application/x-www-form-urlencoded` body was _not_ sent as utf-8, eg. if the form had an `accept-charset` parameter or the containing page had a different character set. +Additionally, the server can check the value against wrong encodings of the checkmark character and detect that a query string or `application/x-www-form-urlencoded` body was *not* sent as utf-8, eg. if the form had an `accept-charset` parameter or the containing page had a different character set. **qs** supports this mechanism via the `charsetSentinel` option. If specified, the `utf8` parameter will be omitted from the returned object. @@ -221,14 +217,14 @@ In that sense the `charset` will behave as the default charset rather than the a ```javascript var detectedAsUtf8 = qs.parse('utf8=%E2%9C%93&a=%C3%B8', { charset: 'iso-8859-1', - charsetSentinel: true, + charsetSentinel: true }); assert.deepEqual(detectedAsUtf8, { a: 'ΓΈ' }); // Browsers encode the checkmark as ✓ when submitting as iso-8859-1: var detectedAsIso8859_1 = qs.parse('utf8=%26%2310003%3B&a=%F8', { charset: 'utf-8', - charsetSentinel: true, + charsetSentinel: true }); assert.deepEqual(detectedAsIso8859_1, { a: 'ΓΈ' }); ``` @@ -238,7 +234,7 @@ If you want to decode the `&#...;` syntax to the actual character, you can speci ```javascript var detectedAsIso8859_1 = qs.parse('a=%26%239786%3B', { charset: 'iso-8859-1', - interpretNumericEntities: true, + interpretNumericEntities: true }); assert.deepEqual(detectedAsIso8859_1, { a: '☺' }); ``` @@ -292,18 +288,17 @@ This is needed to handle cases when someone sent, for example, `a[999999999]` an ```javascript var withMaxIndex = qs.parse('a[100]=b'); -assert.deepEqual(withMaxIndex, { a: { 100: 'b' } }); +assert.deepEqual(withMaxIndex, { a: { '100': 'b' } }); ``` This limit can be overridden by passing an `arrayLimit` option: ```javascript var withArrayLimit = qs.parse('a[1]=b', { arrayLimit: 0 }); -assert.deepEqual(withArrayLimit, { a: { 1: 'b' } }); +assert.deepEqual(withArrayLimit, { a: { '1': 'b' } }); ``` If you want to throw an error whenever the array limit is exceeded, set the `throwOnLimitExceeded` option to `true`. This option will generate a descriptive error if the query string exceeds a configured limit. - ```javascript try { qs.parse('a[1]=b', { arrayLimit: 0, throwOnLimitExceeded: true }); @@ -319,14 +314,14 @@ To disable array parsing entirely, set `parseArrays` to `false`. ```javascript var noParsingArrays = qs.parse('a[]=b', { parseArrays: false }); -assert.deepEqual(noParsingArrays, { a: { 0: 'b' } }); +assert.deepEqual(noParsingArrays, { a: { '0': 'b' } }); ``` If you mix notations, **qs** will merge the two items into an object: ```javascript var mixedNotation = qs.parse('a[0]=b&a[b]=c'); -assert.deepEqual(mixedNotation, { a: { 0: 'b', b: 'c' } }); +assert.deepEqual(mixedNotation, { a: { '0': 'b', b: 'c' } }); ``` You can also create arrays of objects: @@ -337,12 +332,10 @@ assert.deepEqual(arraysOfObjects, { a: [{ b: 'c' }] }); ``` Some people use comma to join array, **qs** can parse it: - ```javascript -var arraysOfObjects = qs.parse('a=b,c', { comma: true }); -assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] }); +var arraysOfObjects = qs.parse('a=b,c', { comma: true }) +assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] }) ``` - (_this cannot convert nested objects, such as `a={b:1},{c:d}`_) ### Parsing primitive/scalar values (numbers, booleans, null, etc) @@ -360,7 +353,6 @@ If you wish to auto-convert values which look like numbers, booleans, and other ### Stringifying [](#preventEval) - ```javascript qs.stringify(object, [options]); ``` @@ -380,24 +372,21 @@ assert.equal(unencoded, 'a[b]=c'); ``` Encoding can be disabled for keys by setting the `encodeValuesOnly` option to `true`: - ```javascript -var encodedValues = qs.stringify({ a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, { encodeValuesOnly: true }); -assert.equal(encodedValues, 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'); +var encodedValues = qs.stringify( + { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + { encodeValuesOnly: true } +); +assert.equal(encodedValues,'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'); ``` This encoding can also be replaced by a custom encoding method set as `encoder` option: ```javascript -var encoded = qs.stringify( - { a: { b: 'c' } }, - { - encoder: function (str) { - // Passed in values `a`, `b`, `c` - return; // Return encoded string - }, - } -); +var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str) { + // Passed in values `a`, `b`, `c` + return // Return encoded string +}}) ``` _(Note: the `encoder` option does not apply if `encode` is `false`)_ @@ -405,47 +394,38 @@ _(Note: the `encoder` option does not apply if `encode` is `false`)_ Analogue to the `encoder` there is a `decoder` option for `parse` to override decoding of properties and values: ```javascript -var decoded = qs.parse('x=z', { - decoder: function (str) { - // Passed in values `x`, `z` - return; // Return decoded string - }, -}); +var decoded = qs.parse('x=z', { decoder: function (str) { + // Passed in values `x`, `z` + return // Return decoded string +}}) ``` You can encode keys and values using different logic by using the type argument provided to the encoder: ```javascript -var encoded = qs.stringify( - { a: { b: 'c' } }, - { - encoder: function (str, defaultEncoder, charset, type) { - if (type === 'key') { - return; // Encoded key - } else if (type === 'value') { - return; // Encoded value - } - }, +var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str, defaultEncoder, charset, type) { + if (type === 'key') { + return // Encoded key + } else if (type === 'value') { + return // Encoded value } -); +}}) ``` The type argument is also provided to the decoder: ```javascript -var decoded = qs.parse('x=z', { - decoder: function (str, defaultDecoder, charset, type) { - if (type === 'key') { - return; // Decoded key - } else if (type === 'value') { - return; // Decoded value - } - }, -}); +var decoded = qs.parse('x=z', { decoder: function (str, defaultDecoder, charset, type) { + if (type === 'key') { + return // Decoded key + } else if (type === 'value') { + return // Decoded value + } +}}) ``` Examples beyond this point will be shown as though the output is not URI encoded for clarity. -Please note that the return values in these cases _will_ be URI encoded during real usage. +Please note that the return values in these cases *will* be URI encoded during real usage. When arrays are stringified, they follow the `arrayFormat` option, which defaults to `indices`: @@ -464,13 +444,13 @@ qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }); You may use the `arrayFormat` option to specify the format of the output array: ```javascript -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }); +qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }) // 'a[0]=b&a[1]=c' -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }); +qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }) // 'a[]=b&a[]=c' -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }); +qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }) // 'a=b&a=c' -qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' }); +qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' }) // 'a=b,c' ``` @@ -493,14 +473,12 @@ qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true }); You may encode the dot notation in the keys of object with option `encodeDotInKeys` by setting it to `true`: Note: it implies `allowDots`, so `stringify` will error if you set `decodeDotInKeys` to `true`, and `allowDots` to `false`. Caveat: when `encodeValuesOnly` is `true` as well as `encodeDotInKeys`, only dots in keys and nothing else will be encoded. - ```javascript -qs.stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: true }); +qs.stringify({ "name.obj": { "first": "John", "last": "Doe" } }, { allowDots: true, encodeDotInKeys: true }) // 'name%252Eobj.first=John&name%252Eobj.last=Doe' ``` You may allow empty array values by setting the `allowEmptyArrays` option to `true`: - ```javascript qs.stringify({ foo: [], bar: 'baz' }, { allowEmptyArrays: true }); // 'foo[]&bar=baz' @@ -518,8 +496,8 @@ Key with no values (such as an empty object or array) will return nothing: assert.equal(qs.stringify({ a: [] }), ''); assert.equal(qs.stringify({ a: {} }), ''); assert.equal(qs.stringify({ a: [{}] }), ''); -assert.equal(qs.stringify({ a: { b: [] } }), ''); -assert.equal(qs.stringify({ a: { b: {} } }), ''); +assert.equal(qs.stringify({ a: { b: []} }), ''); +assert.equal(qs.stringify({ a: { b: {}} }), ''); ``` Properties that are set to `undefined` will be omitted entirely: @@ -546,14 +524,7 @@ If you only want to override the serialization of `Date` objects, you can provid var date = new Date(7); assert.equal(qs.stringify({ a: date }), 'a=1970-01-01T00:00:00.007Z'.replace(/:/g, '%3A')); assert.equal( - qs.stringify( - { a: date }, - { - serializeDate: function (d) { - return d.getTime(); - }, - } - ), + qs.stringify({ a: date }, { serializeDate: function (d) { return d.getTime(); } }), 'a=7' ); ``` @@ -564,7 +535,7 @@ You may use the `sort` option to affect the order of parameter keys: function alphabeticalSort(a, b) { return a.localeCompare(b); } -assert.equal(qs.stringify({ a: 'c', z: 'y', b: 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y'); +assert.equal(qs.stringify({ a: 'c', z: 'y', b : 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y'); ``` Finally, you can use the `filter` option to restrict which keys will be included in the stringified output. @@ -666,7 +637,7 @@ assert.deepEqual(parsedStrictNull, { a: null, b: '' }); To completely skip rendering keys with `null` values, use the `skipNulls` flag: ```javascript -var nullsSkipped = qs.stringify({ a: 'b', c: null }, { skipNulls: true }); +var nullsSkipped = qs.stringify({ a: 'b', c: null}, { skipNulls: true }); assert.equal(nullsSkipped, 'a=b'); ``` @@ -718,7 +689,7 @@ assert.deepEqual(obj, { a: 'こんにけは!' }); ### RFC 3986 and RFC 1738 space encoding -RFC3986 used as default option and encodes ' ' to _%20_ which is backward compatible. +RFC3986 used as default option and encodes ' ' to *%20* which is backward compatible. In the same time, output can be stringified as per RFC1738 with ' ' equal to '+'. ``` From 881891d5ded0e1386a2cffa986da40ddc75e809e Mon Sep 17 00:00:00 2001 From: Mohamed Date: Thu, 26 Jun 2025 01:12:10 +0300 Subject: [PATCH 13/14] refactor(benchmark): update module exports to use explicit property assignment and change arrow functions to regular functions --- benchmark/fixtures.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/benchmark/fixtures.js b/benchmark/fixtures.js index 0336da31..84b0da16 100644 --- a/benchmark/fixtures.js +++ b/benchmark/fixtures.js @@ -220,16 +220,16 @@ module.exports = { }; module.exports = { - simpleQueries, - nestedQueries, - arrayQueries, - edgeCaseQueries, - realWorldQueries, - stressTestQueries, - testObjects, + simpleQueries: simpleQueries, + nestedQueries: nestedQueries, + arrayQueries: arrayQueries, + edgeCaseQueries: edgeCaseQueries, + realWorldQueries: realWorldQueries, + stressTestQueries: stressTestQueries, + testObjects: testObjects, // Helper functions - generateQuery: (type, size) => { + generateQuery: function (type, size) { size = size || 'medium'; const queries = { simple: simpleQueries, @@ -243,7 +243,7 @@ module.exports = { return queries[type] && queries[type][size] ? queries[type][size] : simpleQueries.medium; }, - generateObject: (type) => { + generateObject: function (type) { return testObjects[type] || testObjects.simple; }, }; From 69e8259194f066073637cc2b0f0e9de5c61b3ac5 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Thu, 26 Jun 2025 01:35:41 +0300 Subject: [PATCH 14/14] fix(benchmark): change const to var for queries object in generateQuery function --- benchmark/fixtures.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/fixtures.js b/benchmark/fixtures.js index 84b0da16..aa48ec77 100644 --- a/benchmark/fixtures.js +++ b/benchmark/fixtures.js @@ -231,7 +231,7 @@ module.exports = { // Helper functions generateQuery: function (type, size) { size = size || 'medium'; - const queries = { + var queries = { simple: simpleQueries, nested: nestedQueries, array: arrayQueries,