From 1dd4d3c57999b3b04f26cad95932a32d611539af Mon Sep 17 00:00:00 2001 From: Jason Dent Date: Fri, 19 Apr 2024 08:29:25 +0200 Subject: [PATCH] fix: Add some perf tests --- fixtures/sample-data.txt | 651 +++++++++++++++++++++++++++++++++++++++ package.json | 4 +- pnpm-lock.yaml | 112 ++++++- src/app.mts | 70 +++-- src/app.test.mts | 38 +-- src/measureAnonymous.mts | 32 ++ src/measureMap.mts | 110 +++++++ src/measureSearch.mts | 110 +++++++ src/runner.mts | 253 +++++++++++++++ src/sd.mts | 89 ++++++ src/sd.test.mts | 41 +++ 11 files changed, 1444 insertions(+), 66 deletions(-) create mode 100644 fixtures/sample-data.txt create mode 100644 src/measureAnonymous.mts create mode 100644 src/measureMap.mts create mode 100644 src/measureSearch.mts create mode 100644 src/runner.mts create mode 100644 src/sd.mts create mode 100644 src/sd.test.mts diff --git a/fixtures/sample-data.txt b/fixtures/sample-data.txt new file mode 100644 index 0000000..7542004 --- /dev/null +++ b/fixtures/sample-data.txt @@ -0,0 +1,651 @@ +79254 +79194 +79191 +79180 +79170 +79162 +79154 +79142 +79129 +79090 +79062 +79039 +79011 +78981 +78979 +78936 +78923 +78913 +78829 +78809 +78742 +78735 +78725 +78618 +78606 +78577 +78527 +78509 +78491 +78448 +78289 +78284 +78277 +78238 +78171 +78156 +77998 +77998 +77978 +77956 +77925 +77848 +77846 +77759 +77729 +77695 +77677 +77382 +70473 +70449 +69886 +69767 +69704 +69573 +69479 +69398 +69328 +69311 +69265 +69178 +69162 +69104 +69100 +69072 +69062 +68971 +68944 +68929 +68924 +68904 +68879 +68877 +68799 +68755 +68726 +68666 +68623 +68588 +68547 +68458 +68457 +68453 +68438 +68438 +68429 +68426 +68394 +68374 +68363 +68357 +68337 +68300 +68256 +68250 +68228 +68216 +68180 +68149 +68124 +68114 +68060 +68029 +68029 +68025 +68004 +67996 +67981 +67964 +67938 +67925 +67914 +67901 +67853 +67819 +67818 +67788 +67770 +67767 +67688 +67670 +67669 +67629 +67618 +67609 +67602 +67583 +67540 +67479 +67475 +67470 +67433 +67420 +67387 +67343 +67339 +67337 +67315 +67273 +67224 +67208 +67160 +67137 +67102 +67045 +66449 +66408 +66338 +66211 +63784 +63557 +63091 +63021 +62895 +62663 +62182 +62079 +62044 +61907 +61888 +61856 +61847 +61792 +61764 +61683 +61641 +61612 +61514 +61511 +61503 +61411 +61263 +61248 +60965 +60941 +60907 +60876 +60773 +60669 +60537 +60525 +60387 +60194 +59673 +59576 +59561 +59556 +57652 +57458 +57308 +57264 +57158 +57106 +56288 +56245 +56054 +56031 +55930 +55841 +55533 +55532 +55316 +55281 +55230 +55196 +55111 +55101 +50957 +50870 +49580 +48353 +21349 +21319 +21288 +21274 +21270 +21255 +21232 +21208 +21196 +21184 +21164 +21150 +21149 +21143 +21129 +21108 +21100 +21072 +21043 +20934 +20912 +20908 +20882 +20871 +20858 +20843 +20839 +20834 +20800 +20790 +20788 +20757 +20752 +20748 +20744 +20739 +20721 +20712 +20710 +20671 +20620 +20575 +20572 +20567 +20551 +20536 +20522 +20510 +20484 +20430 +20415 +20398 +20368 +20362 +20357 +20349 +20347 +20341 +20338 +20335 +20335 +20334 +20332 +20332 +20332 +20330 +20326 +20324 +20323 +20307 +20304 +20299 +20297 +20292 +20282 +20280 +20275 +20270 +20270 +20258 +20257 +20257 +20256 +20254 +20252 +20251 +20247 +20243 +20231 +20229 +20223 +20223 +20221 +20219 +20217 +20215 +20212 +20211 +20210 +20208 +20202 +20202 +20202 +20197 +20192 +20190 +20190 +20187 +20186 +20184 +20179 +20175 +20175 +20170 +20170 +20170 +20166 +20162 +20158 +20157 +20157 +20156 +20153 +20152 +20151 +20151 +20148 +20146 +20141 +20141 +20139 +20137 +20133 +20132 +20130 +20129 +20124 +20124 +20123 +20114 +20109 +20104 +20104 +20094 +20092 +20091 +20088 +20086 +20085 +20084 +20083 +20078 +20076 +20076 +20070 +20068 +20065 +20060 +20052 +20049 +20045 +20041 +20040 +20039 +20037 +20036 +20036 +20032 +20032 +20021 +20020 +20017 +20009 +20007 +20007 +20004 +20004 +20002 +19989 +19985 +19974 +19973 +19973 +19967 +19961 +19960 +19959 +19957 +19953 +19952 +19950 +19943 +19942 +19940 +19940 +19939 +19937 +19936 +19935 +19935 +19925 +19921 +19920 +19914 +19908 +19907 +19900 +19900 +19900 +19899 +19899 +19898 +19898 +19894 +19893 +19891 +19891 +19888 +19888 +19888 +19883 +19883 +19882 +19882 +19880 +19878 +19875 +19875 +19874 +19873 +19871 +19867 +19864 +19862 +19861 +19860 +19857 +19856 +19854 +19854 +19848 +19848 +19844 +19842 +19840 +19840 +19835 +19833 +19831 +19830 +19828 +19826 +19820 +19817 +19812 +19812 +19811 +19809 +19805 +19799 +19792 +19789 +19788 +19785 +19780 +19770 +19765 +19763 +19762 +19754 +19743 +19742 +19738 +19737 +19735 +19731 +19724 +19722 +19721 +19711 +19710 +19699 +19698 +19697 +19695 +19692 +19687 +19683 +19672 +19670 +19665 +19664 +19660 +19654 +19651 +19644 +19643 +19643 +19641 +19640 +19620 +19619 +19618 +19617 +19614 +19613 +19608 +19607 +19607 +19605 +19579 +19575 +19568 +19556 +19553 +19553 +19551 +19550 +19548 +19536 +19535 +19500 +19500 +19473 +19462 +19461 +19455 +19451 +19391 +19388 +19386 +19384 +19375 +19371 +19353 +19338 +19318 +19273 +19271 +19269 +19265 +19258 +19230 +19228 +19222 +19221 +19221 +19215 +19196 +19180 +19177 +19166 +19161 +19154 +19148 +19138 +19134 +19129 +19116 +19113 +19107 +19105 +19102 +19096 +19092 +19088 +19085 +19085 +19083 +19072 +19067 +19066 +19061 +19058 +19050 +19049 +19045 +19044 +19043 +19043 +19032 +19005 +18996 +18968 +18957 +18948 +18938 +18936 +18920 +18920 +18913 +18897 +18897 +18892 +18884 +18878 +18878 +18878 +18871 +18870 +18869 +18866 +18864 +18864 +18864 +18862 +18862 +18862 +18860 +18859 +18858 +18858 +18853 +18852 +18852 +18851 +18851 +18848 +18847 +18846 +18846 +18846 +18845 +18845 +18844 +18842 +18841 +18841 +18840 +18840 +18837 +18837 +18836 +18836 +18835 +18834 +18833 +18831 +18830 +18830 +18829 diff --git a/package.json b/package.json index 9417603..569cc0b 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,9 @@ "dependencies": { "chalk": "^5.3.0", "commander": "^12.0.0", - "globby": "^14.0.1" + "globby": "^14.0.1", + "lorem-ipsum": "^2.0.8", + "ora": "^8.0.1" }, "files": [ "bin.mjs", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bda128c..27f4ec0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ dependencies: globby: specifier: ^14.0.1 version: 14.0.1 + lorem-ipsum: + specifier: ^2.0.8 + version: 2.0.8 + ora: + specifier: ^8.0.1 + version: 8.0.1 devDependencies: '@eslint/eslintrc': @@ -1198,7 +1204,6 @@ packages: /ansi-regex@6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} - dev: true /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} @@ -1416,6 +1421,18 @@ packages: resolve-from: 5.0.0 dev: true + /cli-cursor@4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + restore-cursor: 4.0.0 + dev: false + + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: false + /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1436,6 +1453,11 @@ packages: resolution: {integrity: sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==} engines: {node: '>=18'} + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + dev: false + /comment-json@4.2.3: resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==} engines: {node: '>= 6'} @@ -1728,7 +1750,6 @@ packages: /emoji-regex@10.3.0: resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} - dev: true /enhanced-resolve@5.16.0: resolution: {integrity: sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==} @@ -2315,6 +2336,11 @@ packages: engines: {node: '>=18'} dev: true + /get-east-asian-width@1.2.0: + resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} + engines: {node: '>=18'} + dev: false + /get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true @@ -2639,6 +2665,11 @@ packages: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} dev: true + /is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: false + /is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} @@ -2715,6 +2746,16 @@ packages: resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} dev: true + /is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + dev: false + + /is-unicode-supported@2.0.0: + resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} + engines: {node: '>=18'} + dev: false + /is-weakref@1.0.2: resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} dependencies: @@ -2829,10 +2870,26 @@ packages: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + dependencies: + chalk: 5.3.0 + is-unicode-supported: 1.3.0 + dev: false + /longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} dev: true + /lorem-ipsum@2.0.8: + resolution: {integrity: sha512-5RIwHuCb979RASgCJH0VKERn9cQo/+NcAi2BMe9ddj+gp7hujl6BI+qdOG4nVsLDpwWEJwTVYXNKP6BGgbcoGA==} + engines: {node: '>= 8.x', npm: '>= 5.x'} + hasBin: true + dependencies: + commander: 9.5.0 + dev: false + /loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: @@ -3402,6 +3459,11 @@ packages: braces: 3.0.2 picomatch: 2.3.1 + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: false + /mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -3525,6 +3587,13 @@ packages: wrappy: 1.0.2 dev: true + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: false + /onetime@6.0.0: resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} engines: {node: '>=12'} @@ -3544,6 +3613,21 @@ packages: type-check: 0.4.0 dev: true + /ora@8.0.1: + resolution: {integrity: sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==} + engines: {node: '>=18'} + dependencies: + chalk: 5.3.0 + cli-cursor: 4.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.0.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.1.0 + strip-ansi: 7.1.0 + dev: false + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3792,6 +3876,14 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /restore-cursor@4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -3909,7 +4001,6 @@ packages: /signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} @@ -3938,6 +4029,11 @@ packages: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: true + /stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + dev: false + /string-width@6.1.0: resolution: {integrity: sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==} engines: {node: '>=16'} @@ -3947,6 +4043,15 @@ packages: strip-ansi: 7.1.0 dev: true + /string-width@7.1.0: + resolution: {integrity: sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==} + engines: {node: '>=18'} + dependencies: + emoji-regex: 10.3.0 + get-east-asian-width: 1.2.0 + strip-ansi: 7.1.0 + dev: false + /string.prototype.trim@1.2.9: resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} engines: {node: '>= 0.4'} @@ -3993,7 +4098,6 @@ packages: engines: {node: '>=12'} dependencies: ansi-regex: 6.0.1 - dev: true /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} diff --git a/src/app.mts b/src/app.mts index f5d243f..ccc34b2 100644 --- a/src/app.mts +++ b/src/app.mts @@ -2,10 +2,12 @@ import { promises as fs } from 'node:fs'; import { fileURLToPath } from 'node:url'; import chalk from 'chalk'; -import { Command, program as defaultCommand } from 'commander'; +import { Argument, Command, program as defaultCommand } from 'commander'; import * as path from 'path'; -import { findFiles } from './findFiles.mjs'; +import { measureAnonymous } from './measureAnonymous.mjs'; +import { measureMap } from './measureMap.mjs'; +import { measureSearch } from './measureSearch.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -16,36 +18,52 @@ async function version(): Promise { return (typeof packageJson === 'object' && packageJson?.version) || '0.0.0'; } -interface CliOptions { - mustFindFiles?: boolean; - cwd?: string; - color?: boolean; +const knownTests = { + search: measureSearch, + anonymous: measureAnonymous, + map: measureMap, +}; + +const allTests = { + search: measureSearch, + anonymous: measureAnonymous, + map: measureMap, + all: async (timeout?: number) => { + for (const test of Object.values(knownTests)) { + await test(timeout); + } + }, +}; + +interface AppOptions { + timeout?: number; } export async function app(program = defaultCommand): Promise { + const argument = new Argument('[test-methods...]', 'list of test methods to run'); + argument.choices(Object.keys(allTests)); + argument.default(['all']); + argument.variadic = true; + program - .name('list-files') - .description('List Files') - .argument('', 'Files to scan for injected content.') - .option('--no-must-find-files', 'No error if files are not found.') - .option('--cwd ', 'Current Directory') - .option('--color', 'Force color.') - .option('--no-color', 'Do not use color.') + .name('perf runner') + .addArgument(argument) + .description('Run performance tests.') + .option('-t, --timeout ', 'timeout for each test', (v) => Number(v), 1000) .version(await version()) - .action(async (globs: string[], optionsCli: CliOptions, _command: Command) => { + .action(async (methods: string[], options: AppOptions) => { // console.log('Options: %o', optionsCli); - program.showHelpAfterError(false); - if (optionsCli.color !== undefined) { - chalk.level = optionsCli.color ? 3 : 0; - } - console.log(chalk.yellow('Find Files:')); - const files = await findFiles(globs, optionsCli); - if (!files.length && optionsCli.mustFindFiles) { - program.error('No files found.'); - } - const prefix = ' - '; - for (const file of files) { - console.log(chalk.gray(prefix) + chalk.white(file)); + const timeout = options.timeout || 1000; + const tests = Object.entries(allTests); + for (const method of methods) { + const test = tests.find(([key]) => key === method); + if (!test) { + console.log(chalk.red(`Unknown test method: ${method}`)); + continue; + } + const [key, fn] = test; + console.log(chalk.green(`Running test: ${key}`)); + await fn(timeout); } console.log(chalk.green('done.')); }); diff --git a/src/app.test.mts b/src/app.test.mts index 142f6d6..1971cca 100644 --- a/src/app.test.mts +++ b/src/app.test.mts @@ -1,32 +1,17 @@ import { Command, CommanderError } from 'commander'; import { afterEach, describe, expect, test, vi } from 'vitest'; -import { app, run } from './app.mjs'; +import { run } from './app.mjs'; // const oc = expect.objectContaining; -const sc = expect.stringContaining; -const ac = expect.arrayContaining; +// const sc = expect.stringContaining; +// const ac = expect.arrayContaining; describe('app', () => { afterEach(() => { vi.clearAllMocks(); }); - test.each` - args | expected - ${'*.md'} | ${ac([sc('README.md'), sc('done.')])} - ${['not_found', '--no-must-find-files', '--no-color']} | ${ac(['Find Files:', 'done.'])} - `('run $args', async ({ args, expected }) => { - const argv = genArgv(args); - const program = new Command(); - program.exitOverride((e) => { - throw e; - }); - const spyLog = vi.spyOn(console, 'log').mockImplementation(() => undefined); - await expect(run(argv, program)).resolves.toBeUndefined(); - expect(spyLog.mock.calls.map(([a]) => a)).toEqual(expected); - }); - test.each` args ${'--help'} @@ -47,23 +32,6 @@ describe('app', () => { expect(output.writeOut).toHaveBeenCalled(); expect(output.writeErr).not.toHaveBeenCalled(); }); - - test.each` - args | expected - ${'*.md'} | ${ac([sc('README.md')])} - ${['*.md', '--no-color']} | ${ac([sc(' - README.md')])} - `('app $args', async ({ args, expected }) => { - const argv = genArgv(args); - const program = new Command(); - program.exitOverride((e) => { - throw e; - }); - const spyLog = vi.spyOn(console, 'log').mockImplementation(() => undefined); - const cmd = await app(program); - expect(cmd).toBe(program); - await expect(cmd.parseAsync(argv)).resolves.toBe(program); - expect(spyLog.mock.calls.map(([a]) => a)).toEqual(expected); - }); }); function genArgv(args: string | string[]): string[] { diff --git a/src/measureAnonymous.mts b/src/measureAnonymous.mts new file mode 100644 index 0000000..6747a09 --- /dev/null +++ b/src/measureAnonymous.mts @@ -0,0 +1,32 @@ +import { loremIpsum } from 'lorem-ipsum'; + +import { runTests } from './runner.mjs'; + +export async function measureAnonymous(defaultTimeout = 2000) { + const knownWords = loremIpsum({ count: 10000, units: 'words' }).split(' '); + + await runTests('Measure Anonymous', async ({ test, setTimeout }) => { + setTimeout(defaultTimeout); + + // test('baseline', () => {}); + test('()=>{}', () => { + knownWords.forEach(() => {}); + }); + + test('()=>undefined', () => { + knownWords.forEach(() => undefined); + }); + + function getFn() { + return () => undefined; + } + + test('(getFn)', () => { + knownWords.forEach(getFn); + }); + + test('() => getFn()', () => { + knownWords.forEach(() => getFn()()); + }); + }); +} diff --git a/src/measureMap.mts b/src/measureMap.mts new file mode 100644 index 0000000..ce72bff --- /dev/null +++ b/src/measureMap.mts @@ -0,0 +1,110 @@ +import { loremIpsum } from 'lorem-ipsum'; + +import { runTests } from './runner.mjs'; + +export async function measureMap(defaultTimeout = 2000) { + const knownWords = loremIpsum({ count: 10000, units: 'words' }).split(' '); + + await runTests('Map Anonymous', async ({ test, setTimeout }) => { + setTimeout(defaultTimeout); + + // test('baseline', () => {}); + test('(a) => a.length', () => { + return knownWords.map((a) => a.length); + }); + + test('filter Boolean', () => { + return knownWords.filter(Boolean); + }); + + test('filter (a) => a', () => { + return knownWords.filter((a) => a); + }); + + test('filter (a) => !!a', () => { + return knownWords.filter((a) => !!a); + }); + + test('(a) => { return a.length; }', () => { + return knownWords.map((a) => { + return a.length; + }); + }); + + function fnLen(a: string) { + return a.length; + } + + test('(fnLen)', () => { + return knownWords.map(fnLen); + }); + + test('(a) => fnLen(a)', () => { + return knownWords.map((a) => fnLen(a)); + }); + + const vfLen = (a: string) => a.length; + + test('(vfLen)', () => { + return knownWords.map(vfLen); + }); + + test('for of', () => { + const result: number[] = []; + for (const a of knownWords) { + result.push(a.length); + } + return result; + }); + + test('for i', () => { + const result: number[] = []; + const words = knownWords; + const len = words.length; + for (let i = 0; i < len; i++) { + result.push(words[i].length); + } + return result; + }); + + test('for i r[i]=v', () => { + const words = knownWords; + const result: number[] = []; + const len = words.length; + for (let i = 0; i < len; i++) { + result[i] = words[i].length; + } + return result; + }); + + test('for i Array.from(words)', () => { + const words = knownWords; + const result: number[] = Array.from(words) as unknown as number[]; + const len = words.length; + for (let i = 0; i < len; i++) { + result[i] = words[i].length; + } + return result; + }); + + test('for i Array.from', () => { + const words = knownWords; + const result: number[] = Array.from({ length: words.length }); + const len = words.length; + for (let i = 0; i < len; i++) { + result[i] = words[i].length; + } + return result; + }); + + test('for i Array(size)', () => { + const words = knownWords; + const result: number[] = new Array(words.length); + const len = words.length; + for (let i = 0; i < len; i++) { + result[i] = words[i].length; + } + return result; + }); + }); +} diff --git a/src/measureSearch.mts b/src/measureSearch.mts new file mode 100644 index 0000000..7cbc5fd --- /dev/null +++ b/src/measureSearch.mts @@ -0,0 +1,110 @@ +import { loremIpsum } from 'lorem-ipsum'; + +import { runTests } from './runner.mjs'; + +export async function measureSearch(defaultTimeout = 1000) { + const knownWords = [...new Set(loremIpsum({ count: 1000, units: 'words' }).split(' '))]; + + const termNumber = [5, 10, 20, 30]; + + for (const numTerms of termNumber) { + await runTests(`Measure Search ${numTerms}`, async (context) => { + const test = context.test; + context.timeout = defaultTimeout; + + // test('lorem-ipsum words', () => { + // loremIpsum({ count: 1000, units: 'words' }); + // }); + // test('lorem-ipsum sentences', () => { + // loremIpsum({ count: 100, units: 'sentences' }); + // }); + // test('lorem-ipsum sentences', () => { + // loremIpsum({ count: 30, units: 'paragraphs' }); + // }); + + // test('lorem-ipsum word x 1000', () => { + // for (let i = 0; i < 1000; i++) { + // loremIpsum({ count: 1, units: 'words' }); + // } + // // throw new Error('test error'); + // }); + + const words = loremIpsum({ count: 30000, units: 'words' }).split(' '); + const searchTerms = knownWords.slice(0, numTerms); + const searchObjMap = Object.fromEntries(searchTerms.map((term) => [term, true])); + const searchSet = new Set(searchTerms); + + test('search `searchTerms.includes`', () => { + const terms = searchTerms; + return words.filter((word) => terms.includes(word)); + }); + + test('search `searchTerms.includes` for', () => { + const terms = searchTerms; + const result: string[] = []; + for (const word of words) { + if (terms.includes(word)) { + result.push(word); + } + } + return result; + }); + + test('search `word in searchObjMap`', () => { + return words.filter((word) => word in searchObjMap); + }); + + test('search `word in searchObjMap` local', () => { + const map = searchObjMap; + return words.filter((word) => word in map); + }); + + test('search `word in searchObjMap` for', () => { + const map = searchObjMap; + const result: string[] = []; + for (const word of words) { + if (word in map) { + result.push(word); + } + } + return result; + }); + + test('search `searchSet.has`', () => { + return words.filter((word) => searchSet.has(word)); + }); + + test('search `searchSet.has` local', () => { + const s = searchSet; + return words.filter((word) => s.has(word)); + }); + + test('search `searchSet.has` for', () => { + const s = searchSet; + const result: string[] = []; + for (const word of words) { + if (s.has(word)) { + result.push(word); + } + } + return result; + }); + + test('search `searchSet.has` for i', () => { + const s = searchSet; + const result: string[] = []; + const w = words; + const len = w.length; + for (let i = 0; i < len; i++) { + const word = w[i]; + if (s.has(word)) { + result.push(word); + } + } + return result; + }); + }); + } + + // console.log('%o', result); +} diff --git a/src/runner.mts b/src/runner.mts new file mode 100644 index 0000000..074d927 --- /dev/null +++ b/src/runner.mts @@ -0,0 +1,253 @@ +import type { Ora } from 'ora'; +import ora from 'ora'; + +import { createRunningStdDev, RunningStdDev } from './sd.mjs'; + +export type testFn = (name: string, method: () => void, timeout?: number) => void; +export type testAsyncFn = (name: string, method: () => void | Promise, timeout?: number) => void; + +export interface RunnerContext { + test: testFn; + testAsync: testAsyncFn; + timeout: number; + setTimeout: (timeoutMs: number) => void; +} + +export interface TestResult { + name: string; + isAsync: boolean; + /** the total amount of time spent in the test. */ + duration: number; + /** the number of iterations */ + iterations: number; + + runs: number[]; + /** + * The error that was thrown. + */ + error?: Error | undefined; + /** + * The timeout in milliseconds used. + */ + timeout: number; + + /** The time related to testing, but not included in duration. */ + overhead: number; + + iterationCallbacks: number; + + sd: RunningStdDev; +} + +export interface RunnerResult { + description: string; + results: TestResult[]; +} + +interface TestDef { + name: string; + method: () => unknown; + timeout: number; + isAsync: boolean; +} + +interface TestDefinitionSync extends TestDef { + method: () => void; + isAsync: false; +} + +interface TestDefinitionAsync extends TestDef { + method: () => void | Promise; + isAsync: true; +} + +type TestDefinition = TestDefinitionSync | TestDefinitionAsync; + +interface ProgressReporting { + testStart?(name: string): void; + testEnd?(result: TestResult): void; + testIteration?(name: string, iteration: number, duration: number): void; + log?: typeof console.log; + error?: typeof console.error; + stderr?: typeof process.stderr; + stdout?: typeof process.stdout; + spinner?: Ora; +} + +const defaultTime = 10_000; + +export async function runTests( + description: string, + testWrapperFn: (context: RunnerContext) => void | Promise, + progress?: ProgressReporting, +): Promise { + const log = progress?.log || console.log; + const stderr = progress?.stderr || process.stderr; + const spinner = progress?.spinner || ora({ stream: stderr }); + + let nameWidth = 0; + + const reportTestStart = progress?.testStart ?? ((name: string) => spinner.start(name)); + const reportTestEnd = (result: TestResult) => { + if (progress?.testEnd) { + if (spinner.isSpinning) { + spinner.stop(); + } + return progress.testEnd(result); + } + + const { name, duration, iterations, sd } = result; + + const min = sd.min; + const max = sd.max; + const p95 = sd.ok ? sd.p95 : NaN; + const mean = sd.ok ? sd.mean : NaN; + const ops = (iterations * 1000) / duration; + + if (spinner.isSpinning) { + if (result.error) { + spinner.fail(`${name} ${duration.toFixed(2)}ms`); + log('Error: %o', result.error); + } else { + spinner.succeed( + `${name.padEnd(nameWidth)}: ` + + `ops: ${ops.toFixed(2).padStart(8)} ` + + `cnt: ${iterations.toFixed(0).padStart(6)} ` + + `mean: ${mean.toPrecision(5).padStart(8)} ` + + `p95: ${p95.toPrecision(5).padStart(8)} ` + + `min/max: ${min.toPrecision(5).padStart(8)}/${max.toPrecision(5).padStart(8)} ` + + `${duration.toFixed(2)}ms `, + ); + } + } else { + if (result.error) { + log(`Test: ${name} finished in ${duration}ms with error: %o`, result.error); + } else { + log(`Test: ${name} finished in ${duration}ms`); + } + } + }; + const reportTestIteration = + progress?.testIteration ?? + (() => { + spinner.isSpinning && spinner.render(); + }); + + const context: RunnerContext = { + test, + testAsync, + timeout: defaultTime, + setTimeout: (timeout: number) => { + context.timeout = timeout; + }, + }; + + const tests: TestDefinition[] = []; + const results: TestResult[] = []; + + function test(name: string, method: () => void, timeout?: number): void { + tests.push({ name, method, timeout: timeout ?? context.timeout, isAsync: false }); + } + + function testAsync(name: string, method: () => void | Promise, timeout?: number): void { + tests.push({ name, method, timeout: timeout ?? context.timeout, isAsync: true }); + } + + async function runTestAsync(test: TestDefinition): Promise { + const startTime = performance.now(); + let duration = 0; + const runs: number[] = []; + let iterations = 0; + const reportInterval = 100; + let interval: ReturnType | undefined; + let error: Error | undefined; + let iterationCallbacks = 0; + const sd = createRunningStdDev(); + let lastReport = startTime; + + let nextSd = 0; + + function reportIteration() { + try { + ++iterationCallbacks; + reportTestIteration(test.name, iterations, duration); + } catch (e) { + error ??= toError(e); + } + } + + try { + while (performance.now() - startTime < test.timeout && !error) { + const startTime = performance.now(); + let delta: number; + try { + await test.method(); + delta = performance.now() - startTime; + } catch (e) { + error = toError(e); + break; + } + duration += delta; + const now = performance.now(); + if (now > nextSd) { + sd.push(delta); + nextSd = performance.now() + 1; + } + if (now - lastReport > reportInterval) { + reportIteration(); + lastReport = performance.now(); + } + iterations++; + } + clearInterval(interval); + interval = undefined; + } catch (e) { + error ??= toError(e); + } finally { + clearInterval(interval); + } + + return { + name: test.name, + isAsync: false, + duration, + iterations, + runs, + error, + timeout: test.timeout, + overhead: performance.now() - startTime - duration, + iterationCallbacks, + sd, + }; + } + + async function runTest(test: TestDefinition) { + const result = await runTestAsync(test); + results.push(result); + return result; + } + + await testWrapperFn(context); + + nameWidth = Math.max(...tests.map((t) => t.name.length)); + + log(`Running: ${description}:`); + for (const test of tests) { + reportTestStart(test.name); + const result = await runTest(test); + reportTestEnd(result); + } + + return { + description, + results, + }; +} + +function toError(e: unknown): Error { + return e instanceof Error ? e : new Error(String(e)); +} + +// function wait(time: number): Promise { +// return new Promise((resolve) => setTimeout(resolve, time)); +// } diff --git a/src/sd.mts b/src/sd.mts new file mode 100644 index 0000000..4be9076 --- /dev/null +++ b/src/sd.mts @@ -0,0 +1,89 @@ +export interface RunningStdDev { + push(value: number): void; + readonly count: number; + readonly mean: number; + readonly variance: number; + readonly sampleVariance: number; + readonly standardDeviation: number; + readonly sampleStandardDeviation: number; + readonly min: number; + readonly max: number; + readonly ok: boolean; + readonly p95: number; +} + +export function createRunningStdDev(): RunningStdDev { + return new RunningStdDevImpl(); +} + +class RunningStdDevImpl { + #count = 0; + #sum = 0; + #m2 = 0; + #min = Infinity; + #max = -Infinity; + + push(value: number) { + let mean = this.#count ? this.#sum / this.#count : 0; + this.#count++; + const delta = value - mean; + this.#sum += value; + mean = this.#sum / this.#count; + const delta2 = value - mean; + this.#m2 += delta * delta2; + this.#min = Math.min(this.#min, value); + this.#max = Math.max(this.#max, value); + } + + get count() { + return this.#count; + } + + /** + * Returns true if there are at least two values. + */ + get ok() { + return this.#count > 1; + } + + get mean() { + assert(this.#count, 'No values to calculate mean'); + return this.#sum / this.#count; + } + + get variance() { + assert(this.#count > 1, 'Need at least two values to calculate variance'); + return this.#m2 / this.#count; + } + + get sampleVariance() { + assert(this.#count > 1, 'Need at least two values to calculate sample variance'); + return this.#m2 / (this.#count - 1); + } + + get standardDeviation() { + return Math.sqrt(this.variance); + } + + get sampleStandardDeviation() { + return Math.sqrt(this.sampleVariance); + } + + get min() { + return this.#min; + } + + get max() { + return this.#max; + } + + get p95() { + return this.mean + 1.96 * this.sampleStandardDeviation; + } +} + +function assert(condition: unknown, message?: string): asserts condition { + if (!condition) { + throw new Error(message ?? 'Assertion failed'); + } +} diff --git a/src/sd.test.mts b/src/sd.test.mts new file mode 100644 index 0000000..f4ec173 --- /dev/null +++ b/src/sd.test.mts @@ -0,0 +1,41 @@ +import fs from 'fs/promises'; +import { describe, expect, test } from 'vitest'; + +import { createRunningStdDev } from './sd.mjs'; + +const urlFixtures = new URL('../fixtures/', import.meta.url); + +describe('sd', async () => { + const sampleData = (await readSampleData()).sort((a, b) => a - b); + + const sum = sampleData.reduce((a, b) => a + b, 0); + const count = sampleData.length; + const mean = sum / count; + const p95 = sampleData[Math.floor(count * 0.95)]; + const min = sampleData[0]; + const max = sampleData[sampleData.length - 1]; + + // console.log('%o', { sum, count, mean, p95, min, max }); + + const sd = createRunningStdDev(); + + sampleData.forEach((value) => sd.push(value)); + + test('sd', () => { + expect(sd.count).toBe(count); + expect(sd.mean).toBe(mean); + expect(sd.min).toBe(min); + expect(sd.max).toBe(max); + expect(sd.p95).toBeGreaterThan(p95 * 0.9); + expect(sd.p95).toBeLessThan(p95 * 1.05); + }); +}); + +async function readSampleData() { + const url = new URL('sample-data.txt', urlFixtures); + const data = await fs.readFile(url, 'utf8'); + return data + .split('\n') + .filter((a) => a) + .map(Number); +}