From 5fea54cec43c86a3491c96078593210157b9c891 Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Mon, 9 Nov 2026 10:16:52 -0800 Subject: [PATCH 1/2] [uplift] chore: Vitest benchmarks vs. last release and fast-deep-equal - `pnpm bench`: `bench/compare.bench.mjs` (Vitest 5, `node (bench)` project) compares this tree, 3.2.2 (`react-fast-compare-published` alias) and fast-deep-equal per fixture group; candidates that throw on a group are skipped - `bench/summary-reporter.mjs` + report-only `bench` CI job: tables with % vs. published in the job summary - Remove `benchmark/`, `assets/benchmarking.png`, README chart, Google Sheet step; drop `benchmark`, `lodash`, `nano-equal`, `shallow-equal-fuzzy` - README, CONTRIBUTING (Benchmarks section, noise rule), PR template --- .github/pull_request_template.md | 2 +- .github/workflows/ci.yml | 22 ++++++ CONTRIBUTING.md | 50 ++++++++++++-- README.md | 54 +++------------ assets/benchmarking.png | Bin 59704 -> 0 bytes bench/compare.bench.mjs | 60 ++++++++++++++++ bench/summary-reporter.mjs | 60 ++++++++++++++++ benchmark/index.js | 115 ------------------------------- package.json | 9 +-- pnpm-lock.yaml | 49 +++---------- vitest.config.mts | 6 ++ 11 files changed, 214 insertions(+), 213 deletions(-) delete mode 100644 assets/benchmarking.png create mode 100644 bench/compare.bench.mjs create mode 100644 bench/summary-reporter.mjs delete mode 100644 benchmark/index.js diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d23e9a0..e69e211 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,5 +13,5 @@ Please include a summary of the change and which issue is fixed. Please also inc - [ ] All tests are passing - [ ] Type definitions, if updated, pass `types:check` and `pack:check` -- [ ] Benchmark performance has not significantly decreased +- [ ] Benchmark performance (`pnpm bench`, CI job summary) has not significantly decreased - [ ] Bundle size (`pnpm size`, CI job summary) has not been significantly impacted diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd501ab..4993130 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,3 +126,25 @@ jobs: node -e 'for (const [p, v] of [["react", "18.0.0"], ["react-dom", "18.0.0"], ["preact", "10."]]) { const got = require(`${p}/package.json`).version; console.log(`${p} ${got}`); if (!got.startsWith(v)) throw new Error(`${p} is ${got}, expected ${v}`); }' - name: Test run: pnpm test --project node + + # Report only: `pnpm bench` adds the benchmark tables to the job summary. + # Shared runners are too noisy to gate on the numbers; the job fails only + # if the benchmark itself breaks. + bench: + name: Benchmarks + runs-on: ubuntu-latest + permissions: + contents: read # to check out repo (actions/checkout) + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0 + - uses: actions/setup-node@v7 + with: + node-version-file: '.nvmrc' + cache: 'pnpm' + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Benchmarks + run: pnpm bench diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9857e4..dc1b635 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,8 +13,8 @@ We encourage pull requests concerning: - Integrating updates from `fast-deep-equal` - This, unfortunately, now requires more manual work. Use the comment blocks in `index.js` to figure out what to paste and where. - Integrating tests from `fast-deep-equal` - Copy its `spec/` test files into `test/fixtures/fast-deep-equal/` - and update the version in that directory's README. The `fast-deep-equal` npm package in `devDependencies` is the - benchmark baseline. + and update the version in that directory's README. The `fast-deep-equal` npm package in `devDependencies` is one of + the [benchmark](#benchmarks) baselines. - Bugs in this library - New tests for React - Documentation @@ -55,7 +55,7 @@ included. For an urgent security fix that's newer than that, add the package to ```sh # Format, lint, types, all tests, then the packed package $ pnpm run check -$ pnpm run benchmark +$ pnpm run bench ``` ### Testing @@ -171,6 +171,45 @@ CI runs it against the base commit and adds the table to the job summary. It's a report, not a check: if the min+gz size grows significantly, flag it to your reviewers so you can discuss whether the addition is worth it. +### Benchmarks + +`pnpm bench` runs `bench/compare.bench.mjs` with Vitest's +[benchmarking](https://vitest.dev/guide/benchmarking) in Node. For each group +of test fixtures (`generic`, `es6`, `react`, `custom`), it compares: + +- `this tree`: `index.js` in your working tree, +- `published`: the last release (`react-fast-compare-published`, an aliased + devDependency), and +- `fast-deep-equal`: the upstream version we forked from. + +A candidate that throws on a group's cases (`fast-deep-equal` on +`Object.create(null)` objects) is left out of that group, and the test title +says so. + +```sh +$ pnpm bench +# One fixture group +$ pnpm bench -t react +# In a browser instead of Node (coarser timers) +$ pnpm exec vitest bench --run --project 'browser (chromium) (bench)' +``` + +CI runs it in its own job and adds the tables, with each candidate's +difference from `published`, to the job summary. It's a report, not a check: +shared runners are too noisy to gate on. If your change touches the comparison +code, run it locally before and after, and mention any difference beyond a few +percent in your PR. + +Differences under about 3–5% in a single run are noise. As a calibration: +`this tree` and `published` run the same code until a source change lands, yet +they differ by up to ~2% from run to run, more than the ±RME column suggests +(it only measures variation within a run). Re-run before reading anything into +a small gap. + +After a release, bump `react-fast-compare-published` to it +(`pnpm add -D react-fast-compare-published@npm:react-fast-compare@`) +once it's a week old (`minimumReleaseAge`). + ## Before submitting a PR... ... please make sure that you have done the following: @@ -179,13 +218,12 @@ reviewers so you can discuss whether the addition is worth it. ```sh $ pnpm run check - $ pnpm run benchmark + $ pnpm run bench ``` -2. Confirm we don't have any significant performance regressions (check out `master` for a baseline comparison on _your_ machine). +2. Confirm we don't have any significant performance regressions (see [benchmarks](#benchmarks)). 3. Confirm you aren't significantly impacting our bundle size (see [size](#size)). - - _Org members:_ Update the README's benchmark comparison png using this [internal Google Sheet template](https://docs.google.com/spreadsheets/d/1GuqpO0wgPjQ9usx6sR3t0Y_HTmAdRqjXkSjs3SBsmTc/edit?usp=sharing_eip&ts=5ed1642f). ## Releasing diff --git a/README.md b/README.md index 0cb4dc5..2855674 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,6 @@ This is a fork of the brilliant [fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal) with some extra handling for React. -![benchmark chart](https://raw.githubusercontent.com/FormidableLabs/react-fast-compare/master/assets/benchmarking.png 'benchmarking chart') - -(Check out the [benchmarking details](#benchmarking-this-library).) - ## Install ```sh @@ -74,51 +70,21 @@ be for you! ## Benchmarking this Library -The absolute values are much less important than the relative differences -between packages. - -Benchmarking source can be found -[here](https://github.com/FormidableLabs/react-fast-compare/blob/master/benchmark/index.js). -Each "operation" consists of running all relevant tests. The React benchmark -uses both the generic tests and the react tests; these runs will be slower -simply because there are more tests in each operation. - -The results below are from a local test on a laptop _(stats last updated 6/2/2020)_: - -### Generic Data - -``` -react-fast-compare x 177,600 ops/sec ±1.73% (92 runs sampled) -fast-deep-equal x 184,211 ops/sec ±0.65% (87 runs sampled) -lodash.isEqual x 39,826 ops/sec ±1.32% (86 runs sampled) -nano-equal x 176,023 ops/sec ±0.89% (92 runs sampled) -shallow-equal-fuzzy x 146,355 ops/sec ±0.64% (89 runs sampled) - fastest: fast-deep-equal -``` - -`react-fast-compare` and `fast-deep-equal` should be the same speed for these -tests; any difference is just noise. `react-fast-compare` won't be faster than -`fast-deep-equal`, because it's based on it. - -### React and Generic Data - -``` -react-fast-compare x 86,392 ops/sec ±0.70% (93 runs sampled) -fast-deep-equal x 85,567 ops/sec ±0.95% (92 runs sampled) -lodash.isEqual x 7,369 ops/sec ±1.78% (84 runs sampled) - fastest: react-fast-compare,fast-deep-equal -``` - -Two of these packages cannot handle comparing React elements, because they -contain circular reference: `nano-equal` and `shallow-equal-fuzzy`. - -### Running Benchmarks +`react-fast-compare` is based on `fast-deep-equal`, and aims to be as fast on +generic data while also handling React elements. The benchmark +([`bench/`](./bench/compare.bench.mjs), Vitest's +[benchmarking](https://vitest.dev/guide/benchmarking)) compares this code with +the last published version and with `fast-deep-equal`, over the same test cases +the tests use: ```sh $ pnpm install -$ pnpm run benchmark +$ pnpm bench ``` +Absolute numbers depend on the machine; compare rows from the same run. CI adds +the results to the job summary of each run. + ## Differences between this library and `fast-deep-equal` `react-fast-compare` is based on `fast-deep-equal`, with some additions: diff --git a/assets/benchmarking.png b/assets/benchmarking.png deleted file mode 100644 index 95373b4218498dea7bb039ace4ac2e77bab823aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59704 zcmeFZWmJ@18#WB+2o8b;A|QgIQWDY~f=G8aN)O##DkdN$N+U4j07^5YD2?O{Fr?Dm zARzGV0rl4V@mb&B_g(MfT7whUwXYrLKI=Hn{ZvWeE)kRhii3kgBrPSOf`bF$!@gcBO zv{r_av!_3fGA{m3n~VpyDW|eDc__`*<%AdIl;u_eya3uig>8i`&m3I`2&2 zMf4>8xG^M2nX*>T>u;MSoR%tQpdQO-)? zI?8x&5dlHoI`L_7bwFy$A=uDmLL5Kx;+ zPVavH%B#JQ0OR(nCr(tk5!_Pup*i3_R+n5D`0=eo>9LE|??}?gDaHB*2gomKrSBaa z@WllL=tl&6S=u-_@UuHOI9Osoabi;U)G7QbT%6z%!)1ffh*2=78m3y(X7chljNm;1 z4(?-1oa5jfE_hLZ7Y@#`@E{y~@c%jRl1Re)`74Ak>DbTrc$wG>#Z<+mrNRHI_Z>}5 zZJjLaoMqNURzOp|mTFqgTJm!I_w8(0j7{wBnXOLtRiEeT5-Fg@TI!nZg$cmxk`_>Z6ddF0Wp>Q1JP;&wLRPG{kNCiutAzyJ8} z8xNnU{m(PGd2jvx$lreXai<_FcIdxN#nC(;ehVgA7%Is6Yte+E!n-mVI5?s>(h_27 z?zrlC~>PY<|BNd62F$DuUb?k zh;>1PlJBQH2IN4@KF3)Wn6)+Yiyt@7Qp>y-FW?qJL#KVF(J&xW8eb>q4t0&D4Q#|E zp6Xji(su-}oA}T6&Pn;t%%1G&=OjIx{{XF%H+73|_TI_UE&0rCHyk(TNRJaxLDI+P zYMpy5E~~vhRHVJdO8LSsi4}bUgyPJZs;fVTwQ?ihyWbJ{^%;|OT@Vq(fGfCQcbfYx%B2I$je|n$|6^m?6Vi z(T2YLxs{tbUen2s9o3^lMfUs9x%#o$?4`WIkoBB!sDXbXC1eD@=db}9KOo9VJ&|z~XJ#lBQzMn75^474I9sIqa=gf71u@@Y1q0Lb! zo)PZ|218|`<%N=wokZLs-4ycOjXCpR3NH2~zSGuTsqMz8dlOLz^OZ*0P-mZPU!@v{ z>5gRhRGloiwO~%sN7L^_mGI6qEvv5DQa4<_AefQ|qia`bdhEn0`vW+WX67Yz?cqD$ z2ieA^g0?u9Lk_hf3D8~_9vP$(9cjgD`88;SE<*eT2vs{a> z@!mw*Ydq|(_5Szm&-tVgy_@66S~h-es=%b_9GUy#5YkQ0`a@*q z`&+z8HE$CFnxmKo9#i{H2R1Y{HEs5rC-yhdC0bMat-d?RU0@~66~WW<+uPu_ z=%CSkckhvW9B=0yuGoRu4aJ1{>gqtWfuEPpY~>tZyS$9Ne9@if7fg4yNNnDGAN-~n zl1SMpET;DuyH009@qiCN5=xq)@WXs5xgW0rTRZ9OlTNf=$^|F>>$=^gMa5$ROBLsx z2h0;Z>LF%$eyC1*(d!VLV-fJ0V(b1cv)0#NhFuh5zjE3R_QB;6?gdluteW@21t_lb z*^i34S??J7^@~**!SNFDslD?>5``^3T6lVv6MlolFZ*=vkywPs2R_)9rI636$q9Ur zJOf701|dL&oVzabO)+}`=wDadK`>9dp?6n}Cz_*jg`k(1{O8lC1YAqUpjDPVMy{4$ zK0I>$`jjq+=)KjoBs97Do?}emjg)4TS5EimclT}3o#vA}+!H!H`SpHxMBZxDx*+an znQzS(w0~Ahv`DP03H-1TE%_CQY3~iW*G6rzJf~Y1B~LPGed|$Ggi!So%+&a~iP1T& z+@aOv`l=nUHF(3<_VtPrZHeO9-a|9|F;-NjLjB3kFPqJGwj+aGevMe3B006wJis!C z)hMkqjAG?Nj1@X(2EDDi+$kILit^%U&q@#xW%+1o^i+DHpKA*D-Czn+&qwe?pQ*a* z!S&BMYseROYVO~!f8n6oee75qf1XiG5B^{|7G^DLfmMTWd zv;wi5mfYu6vq;HYt9ZrWzKWibstn2ITwrKztM@i@SQxq*Hge3P9Ct2B zVhlZt-&5$v$Aw#`slLxu)o^K>S%DB@;?Iru&%$zkX*5oJr8%jYy%YPrV8thl{h zW^5a*>T4^pwr)>^wI!kz&@BG%RCyO);}E7Aj)a zy!Y*XD3U0Un;Oc^5X+a;Xz=#|*@ z-QS)>8|6d)D+NrtS}HdE*w*Am}ekV!uOJ!U8PH*6?02pz!(1p0#)j@ zwU~6aD8_8~-Tk|6f^2zP+d#3m7jkF8C9A&<3^vN@WUGmBvf7MP4M8$S+{cKP&MY0A zZ)|dU9kFlKkrXg+NmlGm@a%dx961*Xp&+M`W+nJlyH-8uFCCd33fcwr$!2=PK{t)QD#oc@@J7c@>R*5Npz` zY0L6LN5U7~-R^k8n}%Dtb6y4ojple!xzPZTsN1of^`e*pX>S3dvb1*wBO7~xZRNK*3H>|;raZ!L36{Gtp|3r`Ikrh4l3tlH{uiU zQR)Yuy~RG)So7_CU+-2#3inp3C(Ne!oj)z|-9DuGWNT}C@QYKvw#S9fdP`#U3jI9Z zeD31z!jp$sg9W_evhy7`xT=ga7ojWqRBem1+CyrwJr-6@qtgX770=07UAwdAdNRjc zl~2dbUVhC=m&ny+U%yUn%W=U_sli*UK29%z&I84QP@g6|UKZb%((g=Pcok!;n{rdP z$jTJ|vb)i=<$+u9w;q;3b`(L46r3r$YHhO33E7qohwrpNzbA2#bIEAH9D`!nsb2Hs zi8Qn<&hlAm0#8a|7q);Yf@h9l)MQe`if(DTTF&-l!po=2nx3a!TFsw)cBNRJ(0nc z90Cy@sByZo>CuMWr>m(K7li(h)|W8c;0vwK!{RVk_%Z9iw#A^6*5wNvQi!8(YMybibn3#KbFrGdCg8Cq4LY=ayv^n)1Zu>`m2{ju#$Iw1EGK0U&iB2m zgCB4Wxi0lnxv0+dX4`SW?obnS!Np@fXQ@?sy6|iWX{hR?*j^*MAQRA*S79|b;!QVO zp01W@%@uUIVEiV0?5;i=*`yA5P}z-CoTI*fl>}LWL1zf5*io$VuaGss;`p;WPG@_6 z+0&m}@`oU-TorxEVc;3)`S%M%)j*`1a&PSFPtEq{L!OX1*1xP2nh)zhl{KwZndIlV;C=iEa^pz6)z45Piz{o4sk8k{!d_)Ft zpCv!l-}x_%vx1S0rO{>JAMJ>LUnOxJObjD8%a0K7UtI`rKMO|2DkLWKGvfX80vbjz zG0N{8&i=jekI%u#+-FYC2K*ZAA2XzU5=_jq;OGCPaRV^2dRaxK)Bj~vWWdCzS?m4W z?Ef0t|CaoJOa7-={%@E6|L<}dTL??}^i@iJXNy5}=(8(lp4NQ*^xUTY1$mgs_0xC` z4EZ1qP54qePtF*nio16c-EMLY?~%H{d1?OUr47lXX>3Y%$48FvtSC)19>@W-n4Z)N z5%ngp#uPFLGu~!C^F+npoL7dI!dU4nG2{71IPLZ%7J7j=c-vStZTPzES*<1&mcKVXj#XCP#*;VxrAK~zJi`Q3 z-MjEKy}vi!h&2)0A6n)Ai!RduYWV(j*|NVk{t~OD*B-rz{V(Pz2dfEXUOE0f&rM?p zex4-c;Q24MQ46bPqi#<8$`k(43DGKGo?6MwY{C5h@xzag@36*=NxS*Z&uRW+gaO{b zLcC#Z9X$T`HMxhiFB1`|NQQO+oXX+j*{2w9OfV3xk8&WJek&!-=->M@5W*QAP2-k-as&u&Km|Z8-jF?u z{I^FU`Vfzj$MyvqUH_qYUnK=XpGmU)hphjSa2ghD`ogqCiQ(T5JG%af8c;;3D&xOW z{eLtQP)Y(Cj~=?6_Q%)%w5}2mDa8?f`qvjn8!tdK0Hm<`zHhG~T;|XLk2!n{s(mZv z->`p*tOiLtbgFcOoX73cIXXqnX6*?!nACKTo7O0?9qPDw)+dn*_R7@6#H3j_N9z_0 z%rJb+($bRK;j4shfmtxp9O=4nlYxOjRhX&|WHR?w8mQsr+*Umn)x^exUudv;K{Nro zNarP?e=3=C;-a{9)PCPP>qDuR@S;)Knk7>SUMwy3iiyHQm%Zk1!TR!(sqAZTH!-k0 z?a<3YIhuI}#(Di!KFG?|W+oPQ_r@!awH`r9=K&vpw&SOH~6ZDuD-U+ig4}g@%56D2UP4 z;t(w5ZfNo5!hr6<{@#P;al*qP>=PV6i2R)B3%r|ep9Lr%I;<7l{pIfmi zgFM*ZEwE_<>0g^@J;$f7y`Hm0mg++7RTq=xwU)`LP+hsES9a3_Kb*Hc(6H= z;k-M4Ry)+5QHuQa1!f%+0HP2IjthrUa5cwp+svL;%Sr&5@O;+l?_K4t3$yKcXXBvt z!XQTsl2;yX-@9a4i?Vaeo8b!aR)@JyR_a3uS-tDe#kbb*2KlFTxN|$nedQ~<#LiG) zXF6qz@?lS7V-c(u^RD6YoE;MHHN^_A-LT-nt);s3L|-eY^RQzJ^DgrM(#&ru8CPK< z5oFY;e7}8p!=fHvVU(kAgN_bqv^ZGi^kDVXP3l|@ur;(nT)hyXDEtoO>$*vg8z}#&izo3+xG?@}AJS!=Y z;M^sxQEaW1CSC|u|9x! zm1l0x%*<$;PVgRz>?sKQfm-O7HhPLb=bOgic;Wfo%B^7-R4W7kKnoxCJH$`-cO;85 z+e48Og|fRJ9uew`u7d=90d1&YS8&pNPi6!bqVNcMh4PFmuU%^wvAZr4nGX;r6i6wX z(MrBxTOd9F5Q3@40Z45R+#%C9Q50Nxs2j?GtkAyj;Usp`1n2<=ua)Zx5lOgAqDI#BQh@1 zD5tL!u>QlD`?%=w7q{3x z{Rw41WlX9`HlXMve_i;X+#p$6^%L1mEh8w4r*w)yWm1Y;tC!zu6Ws!kly~JWa~MR) z@GRBmRiIe>c!Dd`M7i`ZpeL61Q)^N^gDw*aQh)R4&DDp;_a5c-wpnYWK7Z$#3i(YA zfjYcRU=b_}A>kU*qa`j^Mlm<;6< zIa=|`Lf~?Ht$ZRwfcuB93?DjytIGaiaobPY5P#T^fDbH5ozHspoPr5;tj1GRfWjI= zO5P);;B53kACRyb44yc3(TKuf8(^6_APKBzq@>yfQtMyar+hLDtE*x$`!&Z-m&!jT zG3mK+2Pzx{&s0q>7jKK;As;}3-8&m|Q}DjI@zsfzAxvB{Zf6qV_Mw}3h9ltGh+$XY6w$|d z4U!jNaY+;cxNR_f@;pQ1uDdtmn3K>y=UcQEYSCO0=r7^N`K5|}_|R-ex-nF&x#_~GgRl$^%Ac1x@Z zWdwi{@{nv~m}&CVMvOwZLvo?tdhB%4q@fz=V%o#8LK5543y|1QVxP4)O75zrM+ zV7T0upG7Qoda`3?ST9IFH-=G>%Reohc%?fZsj6@b5tU-3lB0TafOM{0)fbI7*Nfmm zh+|XTm^ePCLC$0VaQ6cpk+^Z^Y#e~I4do^*YF#1YUYqll0E8@A7n(V%7w>X``IH~P zJ25ZFIpzTpmk+!@OeBNq`zn8qpIyVjb6~Xx31mr54ErMGZOD=QN&xxw`0#r7cGdS) zSssc-puO6BjIUf5+~`roqNv@rewcnX5zxV?#D!6VTD-NX_LBP{eD#YI?$OuNuOTBa zSJnVapxB_&(-di|Y%jfAwJ=!L&G}&!bk?<2KHg*Ujk%tv<2R1Fg?dW6!Dv4GM9$`Q zzy@$bIwyFqHZ3*-E20JbgN0s0zcw*xlmCct0^CWBu9;VC%3VgoK0G`LA0k{6sCn~P zJ}HCuyoH~;7ZQNzd$pQ4WqA0AkW5m20Qh2M`g~lT-Lg6=Vz|v%-$)>G%x&Ro zroIp`snCv)7z$M8r}anr`qOhVeA4I$R|X4Dpon5!DRK-d%k@2H(p={dCFL8$mACKr zvkZ+rXRWgD4n^U6y3BNa3L>VmqP-iko;5m*dK~k1Btx0Dw*;Tc`DIJGK53Ljo`DTr zymL6EScb2%2$f5ZGT8o{y{wG7wEK}J@QPVVW@h_3BA#!Fcjj0OM-N)#1$uWj z7hnqmC2ku%drM_gmj^05oQ$})w%2$|mK zj;ITI9VWjLa>Mem?6${&0oKRUe378cY%@v~t>`)fB;73~Ot;87O27DKI~NSy8g!aE zVa8iWN9UD*m09zpNq=}ZP}@d~nz?aQj*VAXI>_DQ+W(Ppf{!{$W^FM!r>S8k&3ADE z9+J?%??!p)_q9OGKMO1gQgU0r4!(Hn!yXi^VS@kVJL;y zyh7hpd#OCNzK>yf>+_|(>JBR(aS8))6dZ{JqN7M3?35BTk`QInOT3!K1)EiaQ`+cFAv|yi#nYmr0-L)xFISqfFY>{gcD)sM8TfVpZ8u=DU??pwV zy4Xk)kZ9oU33gFy0o+DaD^|4fydKDG)l&Ok?eS*5qMm#k0$iGVXRS!(pQ?#IpkE)6 zKjcpaRFFxn23zd9N$CzpQ;}WAk|Ne+-vx$2FFd+|*iKJMcQw$+Z;;h#Q0!&&cWUG6 zb$?t}1yf6L^+zM4NqD>0bs=4N5rK^IF`Nzpcjpd9D^iWZ7c)hzU8PfINh%1VIc3-e z>sfWClv2);er%D3_Od`QSdJ7xiQHaJ0kKzM^z~D7 zZ%~}pl~bGy8CjWMW_4L@21`3hE@||C@g*NXy5&^@;lhTenuZ%CfrTDnoG}{2?Lu2| z@jjdV%4C^11%X4AUR%VB;$jX$;sJhI1kGob?}(We$>Q2$^O2$=DV)&3-3OY+HuxI~ zdbp6F^{?a#gXT#Yg?iPASYUilL01hUA#hF>tl7JrSoBq;*T;j=&N4h$ofBa%APj_% zlC0jL6)%7~PMSF@q%L$x-3}({SvQ1KlYCbel}~|g2w_Mpk(@+T`bgI|cyZ-RN%$wU zF^fL9MjH!&Vw8|6^S;+YF@6he*^{A-ILL?$4?kBZn<7~ibR&3@NFG|n*qC`1X%!ld zH&0=_4T96MDT&%nD^ymQGBUZ>Ys6*0g|({HQ6e>g8M1(P_l0M#m0Biu_tP7VnG9Px zCF&`+xO6n7Q_R|V$6n;)D#S+z2Y&Ii+^K?Bme`^&g=Xfq;gX+S=g@7C9H;Z6YE*@n z%|};B3@SBpw2XJ^L{l@rOc`PldGJH4_O>voQqO6q@gOWoiH?33+?g{(_X!^EQo4pm z36v(M4LhYr&7Iw&4qZ*(AB6l9y9C6YB-y4(=dBo3=cRzSaK>w=Fo{QmSRPM|=H${+ zts(8`l94*dxwrH| zve=O6HK9o#I&;m&w_O-*r9dfb*~L5QHh$ta+c6$3diywEL0!)i3B*=917cSeLvjap z?ZgR5PLxFK(!(3U_G)F&he0a7(UjiH3x2%bW$~dxhT>D^20N{XnUkktk@TEj78UfAm9xD>J>IAoXiDhGb))Ihh}I>OkaqW({cl z(oUyg2bAob>Ky>sHpJgw|lnuhgfe*f_DMlQ#n43AD%#b&LN)kI(VjPLq?8#7I zwbUuyxlMbi(s_!?4J58A&GIT%pSd;KaHdshh*+t*7j?=)%-- zjclyb+F~lM(IKwG$2#E+)Lw%^x)>4$wi%C3Otua!2PQQM@yfiTAWzg*1E!SVwOD2w zX>KH7l037J3^Vj}QD)PVbUe8wmo21oUTtpbL@ssqM(+tCigD8O>$y1T&E1Y?caBK_ z`Tiy|>{$dY9|7K>ZE%~tUlM`li3Y5sy}gldzPIxheGT*AFefj+qe(m}S*<^Z477&_@LjoXf8M^bQE zjjviz-G$^AI+dZ_dz?*Ji#8D(>6W7u8<{Q#B!!(ZQ54GXFvsMSWoN@E-2x5yXwBY2 z+aV0_+E@>!Ir8SEMo8tW+%y~g##Gq7F1%3%uInJ6Bi))VfHn5VTVpyGQ))h z6&iUYszsIZ7Q@S|s&2SMOyK-=ZC~K2vS_7qXP@R=)oA}nmgx6I(%9;FQpwEAsqo0i z@xbU|DL_44mZv23kWO@w^U0}s+x#8G&x4p$gUg7{%r>jS`G5ogp$O=@r4s{?`uz?y zzv~bYQv2?%lN83&8&ShW5k5v%J*8nt1u-F%w$GdpD|9s8b=ESV-|2gq;fU`h$cQ52 zCyQ9jN>fLtKwKV|j)9xYQ*zFf+sle%oU(hEj=sJjsdnO(kXh?RH^ty{#Maf9ihQj~ zNqBE-Oa#f6NAE}@3$qiTk#im>w{ySZwx7#HLKJYd8tJJ}E-j!7Gq*x)XBbIoh(!S6 z*=ug*&ccBlwD@A=c zXz8X}6Yj^BSQqbX%*Nu`^`~hSw#>0^L`beNuw7tbSS~Tz$J;gU6hXiBAS9C_j%Mw; z4}FD=3(hVrW?5n=7BY>p@T{RRlj*e!+G`xw@gb0}K&x5@Q*wQF zWmZP4@cDjlC#z`B%EN03Suc?Ev@%&tetbD*;rdQVM!8j+st_yR3_f$jvQd#*_SW9G z^-`@gh^v$G33@7uLe;&#zWbn}uc^3K(ffP|MW|k)+|PzLFGqFak@zDS04yrGkVW00 zNd_3#4K=s!zKS!AmM=M27p2J|On|>`v{wf7$#^9I?<#oGB#wj3y(kID+ZW;GO2GvDBAR^=5Glt zVhYe#Q88Ht_r@p`1KmK^X{sPuh8`hNBAXWnxa_JB7dHbJ`b&idpmfp9rcv#JwT#Yu zovv;1=1eE5t!FHe?<;R0Bd#+>7tHn=S~6s27VpfM+YGdR>Nj?CU5F)k295J-G_hR`wc7-y#jDK?I--tvo7TLqYY7# zDo5Vs2ilh=O=zpyYqBdg8YQq*5+)~Cw`}jJ1@7uS0zs)5hc zl!~{%TNb;u>FgpskICvHuTNKOrQMqn&v{`-TfxUnT)g1)s;mw{Cil)WPYCAJUeVW5 zkF`!e=lLXUO-$U$ZWQNHwmabCd3vY8cf|`4?cZ?~PUckDy}aI=8F5mamR>c{1EXQ! z<&yy6b%{84_mVB_F|iY_g3m~iV~UN3Ph{G#S9H~hw4gj>vF|N z@aj5V9DnibD_mQNbm55K@_AC608@U79xFV;y917&>$~M!5jE>>qV$}}WzNb=Bj*%9 zoK>A=+H6MlRbTVe@MmWh9{g|>73*ECfAFd;KZy3RR`|qXZ<-L{ z!8@yxq46c$D9yd5+E*$CK?kpeuSMUql&;!HEO~KIM^U+&T4caKq@%Fc&*&;hPoG#t zlC9Fu?fYH3!$JO0NxT5UWN22b;V6IjBk6UOV0^J;Q{v3q;2&$IWC3wy(X|^8`Q8{l zhboZa*(?da>zZFV$J1ASy83eu{9IeC?o$os9Q5?oO`mPYfp1+c8vBDzi0#xo_KCKp z0Me2{vx)L}Q~CWRE`fY=vJpwC>i%Ko*fw_d94*I_e##RVuA&zU0y&>rn=~fJBW&vO z)2)~4gV9FwiW>X*X8YbdO07I;K3mOPVh!upX_?!eMfGW#!B1WKjlPxi1Q3L8zNL~a z__5kGIg542a`ZwAY`!Qr^cAufS(_mwi_QDlVJp3PZ`@tmW6jg}UEZ4twHK6`8`{b) zQd>D;ic7*5P8T@hbG1f_bRC-$p}a5Kj_i=(w6x}u6tofWy?lR7k#|Uc(IzOBRX4qL zC3yTMjNEGrV_2lq|Gt*BKj;4E_wncHwBw@<3Ci{B6P#uF@*O*FZKXL|4j^HglVg{D zgZh%5V2K@HspQsS z>$Y@y5ay%C18iUUn1+*lJKi~u5VSkqy>!7jM=w^=R{rw>$NrFw(DXIK-}VaqSHPfn z?vCFrc$Bd{r%l7a_vu*_3#r7*$hi3`GFjq%E{m4-YAwg1@+v3k{qLV{ehSZ4dslc} zcCVr0v<^?fLdb;c@~i6_Z*?F`%f2I|dI;fp>4EQNlSCn#H+8H@75IfSUc8SFEnb(8 z4GdY>89SZbH}@gwepI{Az~*A#mw`eH#SVuflj{XJ&jxD@W^6M3g^ts$Js zz7#l!d2FQR*wHyNsW-ds?IY6=n}~PhVp-1KUa%tP;)MjRfeWX-Z+4TmCmLu!FttIYwh9M2O90gsj2r~sa3@Z6ncy^6)$62k;u96 z`b2o?%m$I~&S!`wsl*VUYmS{yhp~afqR*Tmq4m(-gPeUg!~Ws6SOma{i(wxmba!gH z|D)4q#^oXt;PFR#;95paTy7=8#OgXzZ*u%X9M4Fx`~jWp)50R%`8=z(9J#R=jrU^u z1M3SW1C(YvZYU;CBzv*%x?i!=3g%`@_tR=v$-8IzA^B7sQ?JqA1`Tz*r&5Z?Ki9g} zkW<+-yy!2!|0OFw`X>2$B*K06dbWG(*^X*LC~{j?ide z)Y}|fJa@2ZLm%m5Td=V93aP2F-3-erw>MfzWd5O-E!wlm-)W4VG^W`jciQyBPHjHb}e~++H0jA zz{Jm) z{51SW*GORLXy>l4{+f?}ZUyf|2eD-y1G&lf{`mT*M^AI+Dey_9A{+k+#r|mB0eG(* zw#cAo|AjSt4Tuh{^3~IS@7hNMXuNcRg7FX-{iET--h^Q}#}ZEk|4Y|OK;!=#_^+k< zHGlutuXvY*4mdXA=q!N$Cs+TP={J>Y4}SmfTe&qTjnY86+GK;WF>m!37_h-V5VD1He4)n0`OVS5-hJFEQ2w1cqhhHJc)czTmnR#vv)>3KSAm39uu)`Fb6 zfUJy6bEF?Amjx(rancy5NX4?!^0yzbo2^{edt-nGd;r~=II3lHdFnmIEBJxj~hXX4cKnN*8K|3KRIotmFoD~y*fB9nZFo4$b zK{BlYVDn}G8!c=XIT!?(%zZ4RV3m5#?r{h#9ViJDA7f>8Z7FK zr6|B*tjKxxXp!!x^rw4rw7bjQmTUp7VJ-S)SeUVq1NA1{^3*bUDi9Z#m z#Y?YhP=P)VkZc}^40$K{$7XEUmLv+y?F9k>p5M}_Kn9_qH6By(WDcfNZL#K{D12i| z(NB_7qTazfE#Fdr{Bn?rz|c1ZM*WYm2Mf1JBf+2ruvOuD{;E9m+V4}bXs++h z3|(N{n~#S%i1BJ1%F`47!HRkYG7*(M1Wh%Z=ASQhuh>g>S4a6_f%6T{`(cG4*!uJo z79CnKG8VmyoGATAAS2lfm_ip^%JiOK#qp&Z-iz^8AVz=k)Xy1=g7U8tfY$ecLmLo) zCdTctYNA_c5vK3(iPYGpP$M1w8Wdb%mZ}eI?_Tyu9r9jl+dms&%-Z~=7VxavYysYQ z$)JdJ7tor9q?Ay%*iaeDb>NJJOYTpOAY8~bkV^6&Eo1&;!zqzKK=#vwh=P+1VVv3K z%gJ8=l!FE!&d9Y@PQQIV9|Ox>QKTIyw2Tm1ZKTDX5Ksf{+z%Hhghm!Cn~PK`B$wof+uq{nbt-UIBc0XLfH zL{9a+C$u4r>I@St(Ir?sCMz0!j%e_ckS5QUdsxX2&;#`HR#IxRKX)s^aVWiLX4^x& z2)Nhd%Y=K26TK|+Iw>4_o`8#m@H^P&c3&Q~Pv@Z0MYtVoEyp>V8RJEHs1dz{Svo?wYY$0zSC{Mqi9`71+@OsM_NF=xGyq2axxQP zKxnNZq!`BL^rsvlQqio2i@DtyO&y#rvubXD?Q8~Ur;j_jjjLf}v=IFQ8K9s#?^4@1 zO}4)C-PjUQ7@ihO?Dck-rAVdJ_Aqtq5DlSUt1qW3+(h<*=F=(u&) zPrY5i958vgExWD>e0g~OeaovcryZV*>hj(^Ly?AFN8DdJ12EO}+`l}6=;AIOvRKdb z;+2iuW66yrqmJs%4S;6pId|T1ok@$vfb_T(_85pNwM9@d?K(I5an)?Zdo37yG71{h zzs9M*Bp|YZcfXcuvYVE&^h~{)0 zm+H=qB4t#|XUZ+TY7Q|8^O|gZ_C zKU5Ge!s2fE5_ENS>G|KOtq;67!3%UyiK?-y0Ce(IlH1ZS3mdA+XLsxZWv9M4o?JeGxXfVN`?FgBS|U8ht3NJzkIsD-3qa17Ty&(lz>SfA4e zi~1tbpPa)?3$PLiuho!`!Ii!72n$6jDrZPv^SjKBzcSV;wNJL>S&f4O9!x*rBu%q) zv&4Bs;iYp`0&C}?eskhChv`h1C^#ho0XEp;>7}#!qBjne+EGJDcTmxdq^i zhXKHiUML+4Su;#6B3vq;V>xh(r)D>Kf~DY3fWtyU0Qb1f7P-L9W7C_s;zvE~wIt-V z*2c2{_Pc{v8b`ZeH#*PI4iqovqcT>O4Dm@@JwS!OTuR^0*3y!sljnp*;ZTy;gNi&p zUuxjx5K-~>0Cw?REWbS76=P|uW?UL*;J4@E1!O=^h-6-9HY3G)d9)Tw@|Wx1ir&J~ zuM_}P%dM78UQ9zjlEkRHu^zC0*;UKJF24ru>Kxg|gDybAnTJ1YVPabtLiQFJs%E3w zf&O*fOMBG8^+@pg^-tInT(JCi{X{F7Eq~l&dS`XYM<0 zqF2Va6zBB$9W1x3zU5rR7PdY?+dSSB>9*uL0n^!PVXd|Xs>FGri$FFF0GTEN=T@;q zE0|NURe$pYtJOh)!)$Dil1n*4P7D_5iha6R_t>qoWzPxKO*(!#~bi z$UEhDJ|N79l{PK(h3v~xYmk5)GKMG$j#%7v4l0k zmzFkl;Jl0Nmd^USoi~OK!ev<;Y_RsmdMqe-bD8$^066(4GE)jdRm9K+>JY5W6Mc#a zN2}I83)n^`;&}uFe3n{e6wM-ve`pyjgE+afIbesh?0Lgj zka#T7cPG;lvj>Q!Q(Ve%XzA=RKM-# z+pHkzA*r9y8Km=ogO9I2xW3g^NFkn3FwcJqD8fb8Y+@outB-IzALu5Tpe(8D}2OW>#-w`dqb90CESC*3>9 zkcof2-)qRSyaOz2c?}{Ya~P_IJ@Nyk4ZFs8Cq@%_&uc@eM!uGZ)$(CH#uG3e`v4}}LW zRi$aENiV4-4am;2R&ttN=VUl@>9tAPXkK8`yHk#{2|B&1u2$87MFw3#+JKg!mQN;^$)PPXklulOd zvdgsxBN1e#zcGJt@E})!^GeerzL*&n2STX=AVFALZFXJum2>U;_4W0aab}0N#9+qK2Tonj^)o47ulBdX3WSG zO)*Gjuzx%kH@?w)lE+3Xh1ta$m_ysKSg8Rn;OFaBdKQ6WJs_mVBJh&vJ&KksLiwVO*oK(Uh;dq%)9V)iOr*3k5PFWmKeN**8JA~BUW`6C%L{M}2MNT2b$WE7D7G`X*ugBM zORHAXYPUG1*wHP42r-D+AVua_#B3%*FQC|+6r2~H(;8jh1mlMefU>2n&Vz)isgb07 z^hdR4h5FDQhNO(MZ@z@Xv!j^~>~ORL#_hMzF2k6pFU@@;?>6Spx(zA(7{ElCe|2$s zu&jVd5g9NwH7zCI+pCidb!=V(RJ$U;Yc%phxlM6dZD6Z_I;mgWXe1B2_y+4=2a@*f zOnnbT;JE#E;vmgwknEhiV#ayL2bdUJgp7@FYNtQo4Yf+b9IZ_B9}`mz$_8WtccK^3 zNp35i{NlxnJyOcfZH=n4j{u?gmW{#L(gZk*MiLiWrY-jD8GZX&h%t}iGwwXMAuJ;k zMATCid5b>N`N_6c-WD}n^s28}q2*TVIcVk$c-`ZsNn)z*?TaP#8QdXOpvX&P;DjNL zR;?0*K|G=)S#i|CMvC+`XQ1kw3!^{P>q5f&z9;xlhR5)UIT3HNlru5Mne2snYsw$( z=HTR`S5HNg9N%x^(b4YS7Vk|XiWcd*uT$p=3^TkY{}l<%;mS)ezT~NnU<-?2w4;!sWGHG0eC^sw@(PlB~ zUuxmV%OklA@;$d^nQShk1lxnEI;E136k)HtFxtC2OIc^bbndF8oCIYj7CfJHYiS@| zoPmnH-UoYI``>{hX^-@PJsi5#aOplst2n284G~!6EtxA589NZRcyJlgKfBBQvXjK2 zlIdH9i)e~8X8vw3wd3@a#eLvwy3kURPOs0-L6!>xg1N^!IqhxUrtn)GPjn6GeNtPa z?8euWojCzL4wD!it$iS$!pH^PWF0ZPOR;@XzRNQvRB-i)?wXnp-1Ha;Cz>Jy$_w*R zWVIa6aeW#+SJbNyqX^FSv=~e1NIhGk!=$zd<;^}I<{ic1u~JPD2Jyf;LU<;(sEWUuv_PX*dpI{+SCGiCTP4$Id~8piok&SoIQr@W$Pe_rHA(VB z0fMMdi6`-f^)w$ri9`T&Mbh1>C+8IbfxNB{QaC%_>H%JTz(QAbuswtQdOD%$5ZI`IQc6qfp}V`WDNzYwQxcopAl+etba$t8x4?HVLh*Ud^N#Nw z?>ol#=lgSp2R3`HHP@W?yytyg_jPGL#Ji$2&&DN5IjFs8i2^2dDU*)dGVJU>`D?~z z1Wm=yFM()ZKel3@c-vts9QI1pJKoQ=meB$1HPh_%FWHj_q@i#1pp{MHt5{{zH%7CX z6*x_t19#(O(iF_mCe)43o^KjTnwE(NbCt_e+h~OK%+@*Q?+rzIVtRS#uEchyxZ;4w zlhNZ+Se@b(HDh>ROj9OkwbIle*(Db*Jk=$19`_-R0+TZA>z6 zp`q{S>W`s(m6g8p2V6cehIum1Kkuy`2qiMZ#Mu3PVCvu{S*JP$LZY{TR(-o`q(c%Y zI3*Cs)_X7M&RfTqrDdv-Y_8%Ftmp5x-icl789)Dhgk9N<&FD62@XFiPsMpc*1~+{^ z_x@}lW%T^(p+dIsz=zzXRbsc)BSg)DDLTG!I6Q4Qy3gdHC>j#T4nU^y;&xc=^f+#2 zd8oCMs?DH6B&juHctbrWL3i;4x9D0^xKT z-WVZ}#X~W?0m8dt@8YkUm!MWx!m|Yv0Jgp29obqtm?vL4oQH3^{zX5L@_Jq4;P4S zQ2boN8JzkVc{yk8by%Cs^>E(?>mQB`7Uq{kX4XEMcRW6_>Kb&*b!O@x=sHTbTOWMo zcq4!**Zxd>L9C(5aKy^-n&Nrg^ovm{!W6!kH|2b|pEEzwXG`5M?Y^V)#^+>*w)7^h+jEuad^h5>A73*k>r)G^;|F0>X)Y-?~Yr&kMcTY z7{Ai>jxLMhfpor=V2hVgVJc_zqUrM^*JB?ytge^=D{6mRx^7oaE2;SP#)TEX!}9Iv zxiW=T4pRh5suPrs*MM zF)|4Rrza=}60#FbdP}9(N4k3M;0m%SLXs0GDc>InSqI_=T5T zb{HmVmA&#iQ&FsI=^0yZjV1O^#L9|h#m&jGIm%<~^6oLm)OVFoX~dCsb)Dk-m$&C? z2{*oHefxM@cXY#>fX0VkJ~u8sel0tRVb&kzo^0)$OVc-!&aj~pnK4Mq7|b_HsmLsR zJCGBk-d3>kEiGAM_JC>&f0^ck&#!MWes^`EVrWQvIWE6+58G=g9y~~p9w2VKvm!Q^ z#ZAZfy}#rkXu}EOCvuA4l=^29IV7f;cI3)S3cKO#%{MS}(zyjmQDT@u#Y|QnIhG6d zqr(2$4WD#99jj{nnKM0^^YsTHp9#FTLZ79in!jM!nmK{BVo@&sG`2DkJhc0qar@-{ zGhE&!$J2Ix=$3$9{v0SjzSxyaDmRxLdqL7bVVWXiIC}^;9s1VZY5H=AL2lXn^)~jr z726D(bDY_Boi`X7V&7gKwF#-oidE)#M|xSD zile5VCzE`BIObfz%2TV<@)N@^$NRpuO#mcOA=I_X+;Y_^Dk@1{ZGg*Sin?t8BO5B2 zibbxi@6VLMUCw4Xd1G?drt?<{M55ah4y~JYC}$vb_|QJO`&H_u!l>dQR=V$Rg#8Y3 z7EJ1;7x=e?polMbkR7a`zX0S#gAC@>3~oQgh^Ej|dW^yHGMPxCVv`l`_t5BfQ4dUg z>HRX)V>g=plHYbvglQ-)3mWc??5)5A5JYX#4|gVb2_u~IQV$4i$HuT12NI~J^GJg;*VXEO*6RBy7lrH6&2_}+&u^?|I7x-u-m)Kk z2@(Kh`wDxjiudgb&_|mu9p#FA)J4wwD}xlcjmH0WEwjTgwu)D+A@s~3J5}ivij2%^ zT$6}i)A!A2W3o&YEIb{nJjo>D_SJ=?f)rp4YGAAkholl(y55e{bj4J8@YpGJ3PcAF zW6&FuT9dsftza#&Xz>s1It=BO%a%^e@}EvxjECsVMqHfaHPWc!8R{ytkB}6x;BAd- z=c#s3ZJAV14A@sUmd`c4^~yHkIrI&{@;rt*-y3efTW>9bvb}}s49^Q6k-pN?8g9#I z8&t(=5ivdM)YqlMc?KM_z5c`Q`@*<`Jj+<9$jV--Ba0Ud#!s%sE3h)uWnq2w?>DhK zaJMWxBfM_Wk?2b~o-}ef!9Eu&o2N_Kw$xx$U?0Fi6=+v583N>qUmM%eJH{}X53J)= z+i#8L@w;d}y~GV&&^27k>TAa( z9}d-8un&2-A5R%)+tnEFVtpL?HE+!Cwb;d6+ZBpjP9x3hSINnKmH(cqhP8k6Byu{h z{kw{bN3enD)kpk7SN{q^zlrL!5c-rJxc4{6L81)J=!`iIB4xk_eudKQG}Socf5&8h ze`pc*vLXm*<79I+J%8rXS-FY0_&|lxzUqu@p3d8>1Zv;i!I=U^wkE~|ltcdtDsrv6 zm!4{vL)X(P_SoI4s1l@?Dr%!-acafkb+ih*Xj(8me%=GclV5UvzwUNB-~7MUns?dI zn&j;cimT=I;=3b7LRTAgfY$BB3t}QkB@$)s3>2j$iH1y3Cs~b#L9=C(TZgaASh#DJ z8`1u7XOsj#|8T9l0x zxT*gv+kaFWe(7?Cm*T%|P(V`+K=vLUd$#|+YJV7P*?Vo5wEz451ZbG!2j`}0tp9b? zx0dsNA=>W(P66aUdLpK`gU!E8ssF;B;QznSt-<~;Y#9FEywDhuAB}a$OrQ}VfTrd& zfFd|WtoqpZtBX4iT{p|952C^k=^49j=CADnsGH(wBM!&^cyA5$)Zy*5VJhe>S{X!6 zMDa)P$)^4A_g5kesHXr>eU=O(gxTIKijdH$_UFv;Id_!ksea8TfvBLb#oKf1z6vaj zGLewgUnD~K1v-q7-Jp{sW})`M)_4-#h2?(MlilG@PQ&)KwrXHKVYL_jajO3h|2RzY ztcBliZw0tL=N34ZgT9h30=PW6j=%`M^fD<3gMrhj@0%x3D%#H#k2b_>0i+@Fy6^kx z2YJ@X4$v?U6?RR%whSC-yQkHcfxhbBx|0C7CIBD9BxxDy%;O+qlnUV0131hk05K`5 zv22H@=-mb*WoBouO|`RK!szN8TY{?UIOxBmf(<}3XuXUhb;C0}wmu4rfXs|RcUdxk zM&H-xlblF7H=xZtt<5mELLt+TYQ6=3-|2_>w)1XWe&v{}YTud*H`a`hZgeH@S%8$& z5u^%AJy}@|`pq9c3VNN^0qK69Yj{`3SnUAU z9P?8&G~w@xIbpLydUQe5<6?TklXu0G$ctfP74$N3%~1GJ{=oWL0nzmW^^3@0fJ7I5 z&Q_7xy(9>N{z`}B&`YZ>DU77ud7v}@xdjV7?I{3qHN#ZZ${o*8l=cOhf|-l3h8H4% z`*km)#uS{o+n|;9OeG5yuYDGxF=YvGnP9_r%I!(A-w#R=d-d|PcXK`H4xBcx=0~)Y z^lHMNf|118_gp?=u{uLkT;_Lv{4@_v1K$U3FmLz_t*C7u9^I0nW^1^Skq0eqSoDzh z*;{Uhj7I{If{TWYpm@Y&M{Xvu{kO*NcE2wg5cH9PSz(tB9$J*vCo`?`i|>jEPQv>qp0_b$KLk3~}rNe!$FX0SOglZdJSSYX~2u0eDJYeCyVF zwUKxGW0~H?hH0v|0PxwKPBd~c=a^LgeBL&>z@;7O@}cZQGwjMGOUUsJE+VW3L_R~r z;B8~^Y0yTXvyP!^zV+T4v+jU+Htaca<01Fhhg-20K9;~BI?)~&R!ZNH6aHEtH8g1~kP)yc1Ve1x}|UN%7+h!rn+c!n1z^JcwxX3JSEL5w2H zf>VtF5z1MEcGC$*L`4A+48skzr0dC1^4OZ!4}&HZ`}n6bicoPTHFO%&%G=I2TpsTC z{!Z>;67IWeC8&#`>pq1#EXXeobeXTM%v*lH;Qx+=O@x8Sd;;pnur6|H#V=3KA`T-U zux13N80mnce&LbhZxr;T$S%_CKcLVw8yg+sc0(Nyz7&uk$r&DA-&ac55E!)Lu@kdD zB64=^$q@s)uYJ24W{-QEoSvx)NZOn12~y0^r+b5njqL(@SURu6%I$^-4n%CiX>C+> z!Wx89a8TD3sI2j^LeWNxVKg(_P2C$nAgF}%^*%NYl|}cUz{IgiU`*W(8MO413l8S8 zAflGDL!{4G_&@Kn+2L%soOqaH+x^UAS29iGO-WE%e`9qlRZOas^qsVXvsr)1Yo=Mr zkDr^v(|LXA;$r*kCiAVviYL|p(C%$)$O8$92!bFaG64V|RdVf8h;=wvm6VQ8Bpyk3 z&tXAd)8I)tZTsUrA|t?&uUzaKyi&@L)O_Gl&`}$Tr0vv`#+g9fbgyx>Hp{N)`kg3m z>Ju@xqgaNdUCL&zMyO*EHbr>^QyFdEI7rU&*J+Z|*Nwy4L`(RDFiYC3G#ZZULwp;_ z%2pi>Cqa7(5lj+c&adaYJd$hqw;bo%TwTXXumtt``XH=UbC+s&rX1Q4we(d`;(1y6 zrTzfW3o<+N?Q%25fM77uiYRsg6Yz|M1BB@@h=`e#MtL=2CQbG8FeS2+b_duKG2Lt5 z=A8;n4=L5@T!E+XrgDL$9k4!hk2_OxXR9nI0tcEA7CPAA^?;V+Wmk(VX>Svld?xtc zvo87egz<@~7|;$+j)N3gprto+EN(h58T9sARqVz|CnKJ`qI-*Q%M9J@KarW7BjEh2 zh&AZeV}2>fErcOOpgr8VI}=CK6TtF)oDR49MMMicKX~O%YF%`*G;ew)6y*zhpI>WZ z?MS!Tc6che?2>oTRCjs`f}lLmLPg!Zqf^T<9945sk~oEHtDmXY@9WR62(<$Nuw$t8 zkSXB&vJ$p(*u!+>D$L0|LoN5>V3F$RzEzP5N6n=!0KAUDKBY1% zzbfIrCKMX5lnVo%o1P5(m_BR;m1Xmzm#RwT0m2V9`rqC|I^|8dqtAdO&DV))1yRz| zJJF17zc$w@$jNx$r(rpi89}st^RzU`&9BdfZloa%(;xtLEM4!2l1kYil%bEXV_`3;GnX4bdY3NO*;OV*hMdDNsO>Im@W(NLsyh=4R&?V*UPtk$ z)Au(QED?$rILMTBF5QUh0h&Sz$hH)Va}EDhhuy=`Zv5Djob}rRaOHgLVq?`1Z9F7i z^Y5clS6GaPg}eYygjI?j;errmfN(Lp2Z012rswW?hNtx~XTP`4=)leG2f3qy4hV_T zu)CY8Co3o`XTWBcPQ zBq!tTiP5bl7C?NQaC$;gJ^|a2@VpmD=0Kt~?g$&QOj_S4ggT1OmTOUN**kLSMB-t> z19Mep=&#@ALWIO1jeJI!QFcFH+8s@U0%0SD%sS1xWk(59)N~Bzz(HstMQIH$IYURvts!_qJF`MwMbwHXLt#}cLH=~?9j#-o0X$jklO!{N_7 zDK47Q4{iSt&AFeDsf1Ivo;GtK)Ka9L!oM`G-lqk$ph$b_CftyXA{}&IaM6fBULA#N z@&J;$0h3ArHlB7Hr=NY)Vp5%{@@&4cb)yZ;IDDISlj0WGmdOl1L9!#JcOuT9RJ+cu z5RqdsY{l``rL{|!0?}C?v8U1r{%dVdA|#WDwNWu{1QhO4#DlrAXAwdkY%8gpT~#Iq z)pK33QrH|52|L8}#F#2@Z=%|YB<8&PO58))VlS#-7Z$c_Yc>$OUPo(;rH2rw=L1c% zvco940I^N#Ssk&dOTN7trNxkvSkk2GI4zK+lAi^MSzicc?!~CCz3Emzm!^BT60-*> ztn$4If&9|b7nNTv9dr4OS3qa zTR03NH5TY7e%{@Y$>(K&Zg!s+cf0Aa*G)z(DPQw(KvL{~r1IuI~0}}`Y1DJ@~bpt`zuTxKm+98cD zD{?rEXvqO|`WhRFmvkKNpt?!AYwNA@7u{V`ksVXn_y@+QVEV|VpAKLK#k4;XokZ{& zK=o)Hw>6%{cD1;BA2|xOi3(hG6pnY7fT0#8!|*KAJ`0oRJkY*r%rH=lm&)|QlqQ7v zs;YxW6$1yAQj5iF0ZO)U(A6-s2W(rFtUd?Y*v0VU6b-X4%+!12$>y%FFBY~z?mRvb zySgHP5nLVsqP%#VDlr+8InbKg)@Cvb*D6qL7&e0uYSS)@NhIIFQO533Sd`&@r7Koi z63+gEy2X?e*x;Y2H>v7V1ku35r85;pN-kOiJ| z?aU-hxnoYn^F6L8;sg=@4Nn_&-<7YA+1G@l7(qG6V2UL7ysrF+N7G@Hl&?ixH{zSu z8+?56-Fnb{KRT347}1X^+qIhvomGbSS8wMMi=a0g>&U8%A9-@;|2hC3C*3dYZT8iT zlxcrBI*fpoXpr_t^!!!IYr4{rRUoUA&e3UoK5upb6rzT(`Sk8rOu1%|E#CsienTR^ zig#828^^_#504%b8|xmrgG!Y4ITr0K1&PC$J@W@n;CR9$?kvt_@gYV=}ZzN*1(UUCDwA?hpin`%@IkreUj=vJS zeAn!Baf11NeIOckn)i&ISjyTP4Ex~pWW*u!G}#1z_m%`tYFwdzoDRw0#0Qp3zE2aJ z9n~c$Nwgb%Oq>qRXR{DXgAl{WEfxXtcdhz&NA>BQ$H_ECARsgt1d=X`5Nh0{AHANT z?Qg*+!m^NB+sQDiQ01sE5lpm%ji4=x^>}1J;U}7*$2B2FwJTrTigezbE@%{II_nIC z$1ztb9XIny(9QBM?SS61EBFIdn{iMEpSJK5}OL15gANp;q>L)gkBjx zLY9%^oI#)(ds0KoPd&_859?0g=U6RS_mebFmKtDlrT)g(B8O;ck`l@{VA)9M-CZH0 zyC1DDg%VxsD4$mmIs7HQtuQfivQ(k5s%%p@D|S>#gKyNI=rz7LmE8U7rr!yvq-s)* z#TxF7!b?p_RdXMi<2dNH#u%_j%F@S|6d8p# zY4qOkYywi}fFJ&SKi#_mqaSr!@H}<$y7Lod_TpHWeZR)*Ylv+$Rra%LRWinoNb*U^ zCRyHXQV1$-mr)dabw;_L3su;6*)uHkE^0`Mj);p{Ij5fO@(mV|xm!lxpQ&Sgi*qym zoU2)}^FrxTxf`=2lS*EH)c?N|1fCT@4z{LZH23){A*#0EqN4FaSg7Zt!aQ8<0&BU= ze^f_0_ zh;ZoWi%!Kdo@M0fgL7*$ld+C;#Pag56^Qr+&E4@lcHs0jY4@H5_HPBLK9v8T8|K|hQ6dmlnCh;>A^(mey;?<(XYA&)o{pq+LRg0 zVH7ef($X;P<~m>cx6Ct(ey-@(L%PAB-t@il3+`ou>j=o$#8L1XM9v72?FMB}Vw z`PJeqx}PgiFH)Y+{rl2YCD;%lJ$NTHE!H#T1W;m043k|SgTtbg9@(=a7pHIHlN(rv zKlF5SJRdcdUJ@MnHil_finK)1SVCTtnCz$1U`*qo1}v+>BumQa`T?4^kJ%k-E?#q3 zowJaSCd#?in*Jjv@^#b6#g&D^t_cD?g~x0u+K-70Ud(|g?2{u5rxHYYJRf6A8MGri zI!TXu1uG;vClE-NhDg=SX`&+2`>fS#1ch`9okDo)E=mzueBP8yD|Sc< zMp!dUx+*XNB^jrNky9ygd7^-YDi^)5)4-}2+f{cT|=S>l0Cw4Zkm?t6m z(H)uZa|{cd62V-9sN)eMGF|T6O~kfB!J~!KDd&TmVC9Wc)6ZIYX12y?ot@$I#5EJ6 zb4+Th-hUI})9b7pNsfNnWydex#OImXs;pGQ8olbjU=}ty1P8VeA511z?=^k?{EfcA z6}4_uox9SBN$1Q;?H?en>9u&Q_~?EQXMY{489(j=qINp81j&ea>=3z^K#v0rYECP8 zT@*`mR2mB3VP2=l1!*==^fB(xQ+Z#2@-=O$7^N@mt@y0+WOc5nbk69icN5sJw*2qZ znZ(+%OU>&8-2%TQ3}?R-k8e2o4THr9f6m)*F_tR6CMHk0xHRhOC+p90(${7f73>xi zcd&ueY$!|Y?cdQ#LLmvm<$*a#c6z$@IV&VKugnX?(^QA^wDgq~3MHjF2L_oh9kmm` z*u_vMOZnIMhtG$ZZly+Et)mEid-0xWm1Tb*X1XY#!=8pFqxA6M9=rp&3y|X!sz({zw^` z;KHLKdY@x#l4nQ4P6z?TeY5wuctq>ORL~}LE)_vDry#M~@)3gSGKi$O8iAalI z`my&sTUS;`(~uBpgvrMJDT;4;3(y)vk3G1pYtckNX-(6|zjQas8x{mt^qa*q-mZ+N zoJC*MV)x1Z!>h{2A0BGv%}~Lv2mN*Wh62|~7!sXFk;ngiu#tzPWrkl5T&Qx`ecj(r zPjeaYl)6uMF8uSC{|hkXEr4$+S9Oj(a~EWf=aPW%2)H;owv~}q!FDjj!E9o)iFF72 zY5^oHfJbj)%bpCKeZc#>yy0B?4&Q6@g{05`5KdguPO$m9U1m8p5c)Q@)t-GyLW43lukahnpZkQ)%wRY*MOxXFUy~}Rbr>X zLrS?1y|vH%@kIy|NrP+K#+~Lm@)@lJZMxl``3vbvv0+X8$2EfIavCtKfUQ#J?`ib= zUo7n-=aQc1k<0)n2h zHqPxbMRBHVHBr|z0d5LNWbUneT2kA zY`Zl4&s3a`NPqp$tC9Qu@vBMF&{3`vJi&i1G}8wU0f%7+&4o6#S3C*8FBeb%5_G8`7cI3eF|mQk25sTfg0~EE9C8&-+BJ!Ze z8zw`YV$PqFv?_37Y?(ydM=X9+pB-UAorl)(_82u=sQ;X@0~igb*OZ8$aZ+6qS$&7w zbfO_g9;H47>KK4IRU+*hlz^%^z1wE8K7l>z8J+bGY`8VR{m-0JI6(X15tO^r3)jYt zzOw5g#01Di!E|UXyi$TjBH-0@DRfnhv)t-)@Ef^`5(DgOMUq)H+cb*UxDzUaJw;>C z3OYg%{CM9HFT1sH0e2vt76w9kSrd~f4&5Mof|S!qt%Ay+rXw>BrG2v$+; zy4Pe10Fjgk)yc7kRW=UgLpQEOjyR)_t|+FX3jMbZZ%yT+NOncIm-&w+=MqLNieaw+ z(;G^wh;#E|b)`@|?RWJAC@=s)v)N?w(kh&UO=~m-OKBVS3IfN)lwUd@I1N_y8m;JI z>W!u6%J$P=Wz%(K7F!S*Y*qW|r)z*xHbOo=wcKj11K4e%O2gePGD6s2`0H5OyU(ls zNG4pI4`|2bu>W`|C(gHKo(pOkpD?Fz;K_t^P}{A|H@Or3)u*i5o(!r7IdnhPp9z*8 z&4*6m1c0|BMxE%@X2TvIW(jusbl}pvi4CwXR;UW$5z{jg^h;JNhS>?=v)hewy{)Lw zOLRq{p zy|-ksnXvQ>FD(ML+5*_FI&|r0ApOK?E|LJTd={&&UqoQ~w7bW*s0S{$VQ&<@{nG5u z$Hv>QeMkhwf=D$O>s|Z$6R;ZLrJuL&)z{B8kBKVB#}2hV_}-;NM?#cM)s$;Kd_2(> zP(W;O^LZ=%CLe23v3Tk$57A3XDqC-u1~$_hf5skgYSWIhHL1%@@426sez_3DTrvf< zZn#2oa0OzD5U-S-g8BPQ0B$%`RVg3wp&Ezd_`1$lM<-ZFqWI;56`1E$IxQ(tWpf9q zr&L4(*}kWB(&AU%A>Be=mcW~9@xqo-*RME9_yA4*y0s-QYyLW5@-~P$gh5M+MvqD* zn0BnMO**x~d5)a-?}ctA;@V^CoY3iNgz7ERfr<*@#rF$Y73dYkB-QKOS`Y01?f zvOI<~ol{bOB`!K?b-8$J62BmYZRB5*3p6M3K}z;=oLsCXZCNs)R!k*Oi6%wt;(En} zRDp%1XvAt-QBh(zH{RBSGmE`;1>)EM)`3|mtGbeN=yV%Y;?CW(NNP%uS1X;7HlB@q zD^5R??bMCXOTb&`Rs`&92<-@pa|VXEie{ZzqEEbFRg+j5CE~9DdJ_6eQ$4^7Q!dOo z*G1~nFfl1A$9(hiR`$Bzg{aeL_2#ijo0rHYcJzGM)9HPn>utH{vHT))C&Zw|5BzYN zOv}EfqXEkpAnQWdXe)ZfA2rRFegm5-)3x$9BQlzUut{hms0(IYMTr~4vB@&ybFk5R z4i%!LV!p4)@+#`rh@v{TH{gyz$)~kiD{$S9$o_k(?>57q!9Wt_M$Gt4B@3|WZ)fOd z9FgYozEr3M3-G#Iq47m-{(ko$Arbm1~-#j^G*^uH1s z*cl-VC4=&k0_-zpS-xzDml3Sh4M>&x@1lXOp%CPNh)Otj0~M_si_%D?1PC<+;jD>0 zk~Eeo96&S~eu3MV@UJ8DM|2vJ1V;89KCKPf^ckS;!7{+b+R*4j`Q_=a!ABFWFBS9c zjf`SDv3W&&R#;f5{e<_s0qWpzz$s21=^!a{8vxBaoq&``((L zwP~S^UMAL;TNeYaD9uqe{L3ddK*iw6o*>-~P$KBbnQo9%jcN}Dg`+Q(z_>Hc2Oi#A zWe^Qe7bIQN%O(>?2#QuqB5eP-I^{?4tek6N*@wy#ut042|;V z{S=uch%uQ)U7aD|mvqiy2})>6&T(L7W+Z3hYB9?~%v^|e&S z2)rD}K+NG4SCdnO2VaNW(6vQNjrz1fV3({=N6$IT^nXvJ0?w)Vc*7bfMtnYJ0c)pX z0(3L<>ad=5ro>&FS529Ihs43oT zeYwg>F!=3D4%o`$6&EEJ>za@TPs4}~_Ki(I*$&q66C)?!%DuBZ{fZGO<`}^uC$+iW z)1<&L3wRZ1(&q>$N0_|hv9qjCR_{~~$9-=PHT%6DQW)-o09&C?B&G1U%?M(I{ zjKn2hgN5x&wCK|d5RckR)cU_M5WkV1%#==6=Uf5ehywkq%*F%?~(0sHQwp<*D|() zHZE=6E72LTszRqtp5%5ahO+eI?hT8Fa$6bxFmhT|EGaWSsG8_FoCEvW^G+DPDUp8tcHY zd-kfhV2YqHF%%Y)>6p-gLVwS1+!-LELwng_Zd5D^r$WPR4hZ$C%oLRin#daCo6X0u zr^TAW)A`aB5QzzS6!IkRXm#PEc`X(M_?9W0mqUx!j z>%J9ocPeD)4#(X9tIP`GFD+nmq+}W8L_NMll|7nC_QYJ?Q>D7xFni$S23s3EmH}MZ zN>7CTmN`sXt59VSVq&@l5@LK7g)bZUaScm@7^56dLm!jaa8Yps(Fn#n%vW?&jO(eK z07M%HQ4sT_zO^CHZ?PDc(!{2aHDC*W4lwrsXsrOgmj0ftxmHI0>9HBsAMef*U@G}9 z#jAoTUL0a`I*1w$dhIm{At0mpj9r%5yh;=RuM;AL%3hZ*1sUJ0KWmT|VZi}Gu7$;5 z@Pv%cRim`TAz0YsE0BdOKCgO%BlAg*4zVqzV24#zysB19Zu3!I>qNoz?_?@8?QA?e zn;bF2$5G9Fv3d986s58`nrQBK5|r9+&Dmdvm}N-ySyhGDAFoLo1KhfjsRPtj8}fj4 z#YN@Gd{J>bxSj#*G)NvtMksxSL8WF+i?3+dDUk+)m0A^$%DKlq&c+ek&Nt{(2ov1^ zq-NGPRflZI{dyLYnMsVLr&&l0K+umJbRfv2J#O5?zwf|qRjJgZg_0-rkPhQW(SCax zB@ZTKn3B(T^FwCWPUix!tq*qM6Cvw7i0`5_Pi#nnat)|b{jI=@uB(?hLyh7qzTCE1Rp{*o0PQTr^Xy6rhnKBfi?NMn0I->5fO6EO$2yYc!l2swTLl14J92(;(L_6&+?SV+>3+#oc)3uOW&ibJh`6pMzTFcV-XF=9plarv6TjYwtH%QC?q9+_oZjk$V2e?E z(?nf<3NsK9FvQD=oMD4%z{D_!ZY9GhfG1BXS3D3>IA5QNII))}nN`LJK2N(|k4?K~ z*mwN}x(HR$e{cz}cDsXYD=Vgzfa0G>f6r%W)Iz)2y-csa`=w_=45NtTiyPUT$Q*C+ zB5!AKTu1$R)=TMn*{-3T-}qn3lJeejxr(cbO}LHACYPUE2e3QU26HU?b0F5KPK+b_ zpv3qTuSKbJreTb05~wnH=GEuRvd6lb7d*CfmU9TSq?TXkv@?RERJM+7o}25jR(T#% zq1lpI)_H}x2H(5%Wyj77zGLB5b6@E-I6BZM;V>{D7fukQaHOHlm~P?t;yNIj=5^ik z`m_KYl3#h~tVk)tujUKIj_i5y=LgA;Pnu_r=8Q<$rMcVc-%ZCQ&rkh(3y`TIyzBNe zp+UOAEMs0g3lEwu;>u$63&E-{Fmu%0nkWbB$WR^Arw}h8{{~FXT$nY?>Cx67QDj-e zC4qZ!7Bs%T!~0_Lg1>iQZ|1yYXR`%0sdj&sTcEb3LCPS*rlG$8l9L^`My+jgrE=JH z#7auWgsrZ8+|cF`L!HX+s4-|#wz80lP8W>E7OH~Nn4!bHLVDD?kLOae+N*44R14xT zNMecqjYRDv%pBL=Iams8N1m>{O%Ku;_=`V8ETilNnjGjkzoq%J3j$ev!UzRjl7 z;KwXfBox6Mtiy)rcHGiv*b3&#@Vk@?ZJ9<%{&c?hWQRYTysX#^e4$=HiWj)si*uIB zR%$CiRT6Yas3Ixx;yAo+8(+FoZ+1K>2&fGUpr4W@hM60qJ@KGQE^o!$LEXI^!9U@3uUiqTUj{BI7%Y!o=CpO)OR4exHy1>Z3VqV<{_b{U;@6yvS5J!QUPaWv75H6E9V$r#fR7;kuP+VbdPRa0wFotCIo zU23|N9WN=EpXki|)s$Hhrk}^FtAFhMdC&YKz3HG>eZj z$P)3YjC;Izc!X%0U>TKz_?q>qk8%@eRe8*XLzaL|GC!%YS9ec%FZ)^Hc@-kRi^s3v z@+GQSUx`aEre#Oz7yyYgmw=S1dzN0UepAn6m8oo&cPe6c=iKF7Sa7@+`3 zIVF>)p!&$Diz{X8aMS(cEW4E_6ZRrf(~m!$C46(NItK$CE+(ou;w|yM^fM8@9%qMs zr=-7jCXHh>$E8m4BnlLrsIDAY2}gdB&FEcu?X_8UXxdiBzy0MzQZ(x)i=XQ7Zxgml z6!90x%iG>Z{ZX0eMotG;w@wjI5iIFvbmqsp@8tGy<|6q3Pe*XZK4-HF;68!^oUD9M z2Uf4Yjvho3Pz5~e#pST8{JN{i{|c$Y*F3EKC1(Enckr+O#XqLK4u}(T*4gOYGJPaI zaR06phFqIP^ZPLXIH|;(zgPG>cRfmb5HkFyS1UKGwdgP;+%it>bDr2$=<^;zimLX` z!`nCjphRzn{&$_2MNgDp-`0@oc&SDu{Cy%Wbh?Izm*N( z>;C0S{yTBzxl^k3G`c!@A;&-bN9##D`9{+XLBvcd>f`)B6xqFc75OJKkJ$^ zXjoZi4xUrqUZ89+sYFin?r{G%f4UzLETDOH#$X%EPUc8*~? z%exyXaG^n)z55=}z`({vBZ{hi{; zzkWwAJc!5SCy9Uk`PK{C{{jZq`5;Nf7M>o)h=ey}f7l5M$ih_upe11R{O#4PzX3)^ zZhSQUYx-U97)_&v1gEj;>ML6ps=WmD?-j;aL>TDT-J0Hm={Vte;PTev(~QGrfDQH` zN@awO77b&==bJzi{D)8!K3KHZWAGmrmopmaDAzvH`J1cQABHEw)ZZOe{Ywi2;9L5E z`-ivWq0C{w z_RfQCZp7;M;pz68#BS;*gndBy)C4d*Wz13{KhXL*4$oypdLV132l(Ijmi#koZs`|AMUno5}^y^iTN$?nHzkJDkx=?PRt>|4U^l+=~RtYdk>@{$8GdvG1m1@wM8Ve0Tok*J~0ATKRD)jph z`Evxi0*KyVqqqWt= zOsLdWhFJF=04Z|i0Tf0R(GAy5xW- zXr9cW4AuF~E<)P?$Q`eWaaPGne3vUyj4-9kwr@t3PHOv;4x;Hr7w;Xg{pAGgC4Y2C(89s@E@1zzN$C?U`HyGa?S&o zIwE-*;cN8CZ%!|HGiIGWMHqPhB0j;qMsMGyvNK;tpufx)%hk>_n=Jp>f@|vccuH!_CVb~D;Rw1 zGwQ^-eIJOhz>SB)jf+-p-*`w5%e2YWU!h=;z;~wLx%tt}i}ewR_%S9!Z)z)iEp_pT**ibX~!eAs8pe!n2p%4ARutL&e@S?@5Dc%vCS%*@(;hu9F;Q(yN zL&wotrbHJ~RIMYKg+p&EbpD)~m!~&6V>gx@Q-)y8V1S?@?JRZauezidBBZBWhYa$g zw6~IHe?6ugfKgTPJ&~-)I8G2FK!m{(g5?GPtcd6ZLzNzJHgGe42->I%(BGaN+yvs@ z3jJo=AnDG6*iVDLc&Tc0^`jL~h=3}xq;_#ll72ARq= zUHKL7u6|Cx$DKkAE!kf)<^i+%fIw{y6J4utHH%3=deotf{Ds{(^ofr>r*^CkV>>}dLsxglPT_%*qFIvxX5avo?QyrJ}{tFDqtbHI6|W64v9r zs1Ka2+u;2%zFoYZo9^-gYY9^6Ul788sQK5EOb@g<+N~dBW)P6fqy4-b!xpn$V3cyb z=ZF(+6INGF-{4$C>=&6h>ATsNTc>R&+Os8^7>;HDG}!`L10)tVS~zSk!v7hxfja*9 zJ2kJnv{`;*JlFYH3HDLje?a;-17h=>bv5ez2q^S(cF{`F_>diyyYQ=KmHA+hJOE*%APQ$b1bV!@~d_`jAFVf@rg4>tv7I64aCG02Fo@gbw0zPuDY&*;SuN*$+pCG;mbt;GZ8&N*{mGAVsD(-Dg_c0%+H?lAs*h#lgq zKh-;FfgmL&0w@XVX18;68IBmu_!zB)^Zl`1ITN}~OgW1*qM&wBqVbMS+X8TL^|AHR z4|Y%#8iR69MKjM&H!#R>%$$C)!F!_XTE+aPfymi1$+U=6f}-<+9>_qR9TDNjr>aUf zg+9DonrK(60cE5gl^bvwWlLkOR#Q-g%_$fj`nW+a;%o*Xbiu#y=U3=O*Jyn*2$B?; z3`u#lOxgW!%-Q_8j?zj%j?=!Zd@dm9!eLVC-FXz$15JlE3J4D0olRv9qgyMry{spJ zCnhzr)-9**0QRzM8f22a#P(2;UuaY`LV6*!j0%=Df4(hU{sCdikVu^x?23E*w1td# zxC1G=fW$FnqC(?s=mK*Qn_KELdj2g!{3|~OMDNq&`vZURNcv+S^WfyVa^adtUHr8} zO5`5Utdt3jhLxy*zmp^^d#^PqsUdY7?nMx`HwiFF9vViv&i_V;?K*MEBRtbzPPp^P zjoL1ZHdR@6Sjbszz+P46BOfz=Sgi~(88M%M_z{@$0SEBF<3E z=?s))^zH#z)b67l5aXHI=o}Wd>>jX1AE*984gg;GY7! zW<^>355d(jn+$Vu-S3%U6V=fSAvW zL8N=BFe!@JImI^lb$XZiHi@f^t6wd z7j7tie3+{Q>Epib*uSy@+fl33Jsu6Bhln69yL!pdIueKUahQx2n0_T7z!yUPc933#B6sQS<6;i| z=5B%@5ZflxBh`qcyDkG{7w$qQCk&g;8*X0PIc>#MP0zB!a~siU{5!bsu@6=CB^n>8 za{}x(4x?fD@q&iIiU9v*)a~@dXub+LOg61v{gE3hLnFDajychVeMr}(7NX=r=YF+; z>Z9+qAF7J2UTuMfiRfr2MF0%2{0AcyGhRe`GT5QO`|H1IH8Rj%I()d+IJcl5dn^2^@H9989CTklN8 z&2vi80Z9BZ*D&5vzDet)-$|na4G}UX@wqvV;LOkKR0+0SyyWqrZo9KKWRKn-4ml^D zXP~8k+{rNNZV&w&c1H=#aMybXVGHdu<9DeZqVaFqQgzb*1)Eh|LQW)(lWmI1}jR4l(9lkLXq^klx0+k zGQ58)e|&E3>acT}MvVyp4q%}(k(KhWk5uC%{i)%7*VoL_1|tsoEC=ca?1I=pXP|a! z6J*o%y{jCJj{7EDc6fV5kL+{IA-yoJKM&-^fbrRWp(I3Jf_dpC%sJI4?~jGRmN9a0dsn zBW2TW1Wy=b6O1Y6X|G^K1vIYe3Y#R7Y8cW#KoY4IU@S}!WR;H9`}}AQzc%+%v`^sR++U)D!04IYLM zR9{m5E*HvJSeD?|=Q3at!sm-GJdG*B{V_jWJfPH4e#&5lLAF{#vSd6;g9uGDLajfuF|rZoak=%NAV{Xi#L>hwPS>~k zw3ZJ+0QW$YXi97EXY~84+P|JlPbQ;nM(m-i9ymzS$m#LWOGyL-E2ySrg~#?fqUEJF zpapGQe7q+A`EWiqRp=zjH+@|@FMM7?O+>#)oIFS&Ny@MEhRl@MM;?tUnyEpirF2Z% zJZ6KUlxwxxnP&z;MM*XC088})onYY&5&2K8S7Nfdvs{Y;M+&V?e$sWdR?~&}vLB99 zNRJ~%lj*#$LaRlH_V=>b`+SVQD!1ksfOIeF!A`Jn1U0d0R2R;XNTjq++qe<5g)T{Xx7XayHh}$+0qP^! z%5`f@^rtLZb)vIltJkkO$Qjd{?m4MJKf+6t_4TB|vxq}B=l0;ZRQWGz@cX4}+olnC ztXkJ~M~eq2U`I$^NKeU(YOe^iyfm#@c^^m=tgq7&7u9yT01?WlzI7yD_q%yLEKaA6f>NUlx((a>m{rdl{z7-^J!7dgcc74p|3p*Z;Y3kM4KsT5X=ZoU z20MD1M4c%jS6$$TgCmyh(sj*oH~^pYe&TeV(Vf-AxNv|C|FEcFvVTqAYE#hcRv z%hMwzU)A0#>5~|g>fjUH=H$7-_N(lI6bDLap`PgBJe;L8O0Xj32!{;(O&zL9(xdK- zfr1_Rc2;_+R+NC=VidI`rBaOYrUggqhBi3POB^)PCq|u5YksiB%o1j>9WzT zTI;Prt!P2+7kYJ6<2i|kYA`jx?QEUL!TqvffaVNi6DutjCBfFVDLBtKqHF8p-s=Cl%8O zo!L!lZKGytv3E-|WEuy1o>OnqlyYyYMUOoQPO@OK@M1q*D<}rwS{m`UwM^?@xEvdyPh&tIkEtw_3YcM^a zpehUd1f@o5shG>WwZJHBZS&t4a{RIDdO_*Ke(X(&v)%d9FOJX7YE7&mYy8>13LxeQ4 z{45hgj5N-z>cyOtOTvC~>bd+lEEHeENpNqXd+s;B#6uZ|u}wvRo*Nqp4>wY0Ryu#o z%;Gw!oS2i^%)0Q1erO4(0dsA_2Wk>iv--+t;xxbI!U*ErecYpM-&N=Vbem$?}bzM(W zF5->VrM(U36fcSFHZf&lkDEGK{kez~Q4Cnac&HfXPbkDbd17y2xWby!+8>j{ za)Ix-T)8X_LK5F&r$CpoA{%{r#f2?hN0Wj3*{bzEVsV;5hfwjO0^Lux<5`GK9|3aq zgJ#PtE=Dh8YP!S1Hrlii$y=IgU;{JE+DZ$T9Z7P1v)2ZBB$A4{Yx z#B-`UmNXSS;9x8_K?PBEKSbF=@i@4}!+ed7&hN0M{bFm#AadL)0Fm zKy*F}Bf1a|udBg7enO@g3AAGG4pXOaHDaN%U%3=`ROte^X#4^@@Mpx(+P`q? z&30ht;k=QLVC2~ZRRLo*d_VjP0N-pbVC9JVQTeAr3C{tjc&h*YDK-TUj-HBdmS8kk--3Cv^G~lxNQqtdStG(N={_ zUVzhHuI_K#@!LrJ8k7CZe({Lht7cMwBjxl&_0aR5mP zNLdA7qe6ar3x+=+!yS+sxqs=6KF_Q6--FW~#4^aN+8(yui4ZaV?=uhX*hi>34L0@( zw4V%pK7r__R(3ZPHHg&2%bD`Lkxbt2p;#(BWA;x=e z++OMyVk_DiGCCx2=D5bU@TLS^v?e1A_icrNCQI(!G z#R3kSDYeb<0_+_wr#K#m&CFM1FT#}IbFvI)0*HiLTp2v5e>@mNKpZlYwsKD}Fq=Q# z`Y7*{a3@i^66&_SW{A*$%kH2&rm($n{3&hya~FW8;Q|Wz1@NEUbnN=CPtZVoBT~HA)Xsg%?EZNwB z7l${OK&1fWMi$t}Pt=D{qr%jfp? zOW>izG)Y(;C+NMKm8pV|!Nahg1Lao-l6m=TN;#%_Vs|o#ee5y;I`nSTS$XvUIsN$K zQQeHVVdA69LgTy*caKpp>4-BlAk3RCkLIKQxYEcJzy;l#_v8HL+W6H~n}Z>Q%oO9D zvJU^*9zn^+YQk52R*Zn%vPB&pH6UQk+TU^5wW1#JC7AZ5t{Z9CJ?bI38t* zH2wsBWa%KbQHR=Jr$ot0bO8(paj3v|LtFJN&5xC%mS)yC%E1!eJpy8Aq^DB{VVK>OJ(L!?E9MZQb-0T>y@}6 z`l5@U10>|d13y+jrn`XP`cGEoG~9Ll=`O@pGFAy$Npf%uiT&sj=W-ggJok#T>glCm z0!@BN^TduFj50Ep#c#OdBPMwIIXju(?_UPY)B+wn2hviVh@thF*S3L2-M@d$v8!o6 zd9>2Iq=3q`Kk%D?Y^Xw&T;f z4z+jh`;SS+b|1E*>NnKU2QAWx8Csze!d|+()|IQ0itj9>N9d$SdT&}CBJ9LX@iQMG z>vR0{XRyaA@LQMkh&HDqeWF^L zt_e8u4HZC<=%zPNyOilWjuy}M1(FK+CC=ddf-oyQk`3MnPcxp33AY>3ATJvuN1Q%`B)c8h10%}WZww~{22uK!-62H3E zAP*|8s6@4NosNc(3une1xvUPl=70(F;aRs~`V|OpqY`I;ra1~2*Au~)2{~8#?(+?^ zMxN(jsSK&T5KxP+vJEx9NSE?Ki0?HNb&1F|jDC(ZrWc&B`H>3JmwC!-_S%+6`w3Qt zg;OKqhF;F_oAiJhE_oN$eu^){e{pc&YS4-jl>$>9vip3y8I+VDZOZxhzK#=IH)Y5i z-Kjdk8r~FyIDYvVQvSy*eU)0pp$VQIu|&=q?-$3D(z+qyrp_f*sA;~jP#MK@eyPVw zl+RUo)-fvWIVK0_=FYk6H$mm~HW4bG7ic+OnAK~I% z9{kXwU7_8ncf|%2F>R6@G9wtad)9k)AtSV?%^q}6L-2TK7n(!cMDxLmz#d{mR}QW> zCDNygx~`}~XU2bm2Ib=FF5_|y-JS-HsZsB?0^Bj#H~v$IiCHU!jqY7X~7J3;LR6Ho1NFnPBc% zLAv*lzV4NxA~3(a7R)$TD5;}f9cJjp8z`XWj&peQEk*-E+`bZBtHtLA z!isB88h&4E*BHRb8|!&c|6QRK&+R>J zV;Lp}qO>({g%xOY$+RDIhC&Tm;r-ot_+8`Ak#viV?(wQsmh+@Qtv$CLxo?bi52-YoaE zj|qsqr(x>e(Z(o2n(=sQNfn zSb&rS-uN!{5}K0Xrb{Rm`*Dmgb=%;kF30evoyG!b0*udagALKNwhk ziGlrb$nT)zv)YC$Tm&%a=9wNi^L1TFF)}1v`6)5j4s%)AoSoyHJa2Uhcapv8_W{}5 z{7|Rh0;lg2`t+oS7#A_Tr*(_98W?@sh2%xJPAttnc?5+ zq_wIE-b8Hj3RDhh%^`qjk^$fnAd#N{2g|bX+wKs z&}7+AML9I54@fS2&Wn>#HBhM*Kf1zmtv!l7^F#}=X}4Ht;c!2HN7E@@l29&ojc!oMKYzn&m`ZpBCkorpB0YW1=~)IT*%dCJB^ z4i5M!_PSN(I@fZ1IETAXLBCL|1KmPH!c7~6KKhUeSVcG z1k4DEc5utquMZbEkMFuKh9O>j;NA#A@lu~RgnftL)}T+WgYu(gueWHS3UjZ{eGgR@ z%B*trjrGgAy%4D&r{3~SI6$#$!yny2A8_1~!%4m+M^Rw-MI^j?@mawpBx_WYWn0x; zI2X%q*8jdRemBu-h2SV0zf~ErraMh%$33&A0w*V>S@iX8kS>-hOkP`YO&ge7scj>q z1fqn!z$t-*w@$k0ys3zsUAoA|0$m@lxT0*l>6;7rnm=djhOPITzdP*CLNBmi-Qz;z z%yH#K6jXs2+qjy}LN!a4G_U|?1*V_3C_Z7JZ`chTXKH2IFE!k+c3?OCez8H(Q;}6! zGC$%HMiQr`>8TBtMp34f3S;O~#(Nob*jW-_f9+RS+`G}>OL1+9s0_4>C|(lbMUs|S z1#)wf{0-w9{_USmECWH|)*P#2qI=}({1{b%X#Tt2oRrcD5S-9;-u+wbJ&`ba{W%Se zPeG&XmSb&>2Ycy9(bf5Hw4{v-+M+-LV`Fs**)YG>)QK9>;x1kw?Tq{cf{TF#DZ%^VsJTdL3H=W&gT%`BnefJjI znSS9Vl&Yh}u}qdmIF+smU{IiE=Hs+j9cbcZ9plIv4O2~T|6TzKd)?bjHQ&x=d-!MK8Z9CBUhmY;KIhV{ zG3|WHq8~VuRRd}*ObR+>S8B^c)DO^vzhQzFt^H^b=6Hf&bFUgo?YN}W4lFAKp~)wY zJlKr)0vL(d8Q!>yaMKbHZ}ga`YiN>}avnKF+Ac@J4`3nS%iBZ6#2+^Mw>v)Q+0AQv zNvhF78Hi^zTP+gNoZ@@^6R#Ifs7q(7)5{zk#5C7uTit8Di`GQX8liy0SDgBM1)EDRDq!WtA zhtgen2&6Twb%5TuK71@zF{1b+hsybmL*Rgdm>3m+IGioEmO9G`1&&@44s9xG_@c$` z8do59V$PHR<4hYYvu1O?ZhGMnkz^nupH;iddww2ak)b3=p(j2lX>v!fXCN+w$LuA5 zU#~7Mytsa|!5O?Z@&IMvvmfy7Tn0Qa4Q|ns1*N<_=AR%^ zoq-ZnDz?+tCySCf*}-H;Ez6(cCwv{C13+w73cc)>*oubXE{K!y;2+kK-FGL6jG0~X z@(%22q7668o_303Li{XjTj%VijP3><^-td=+5L#4*v0uqi9W%J9}p2i(fJG6}olAXUw(h+JUu zPs4?mrFs>V82DRH1^xyEvXamTSfm{3Deet~qcB|q5UGs=fH8mJGDlAuSuVrC_ko0B z(rIy0!}c%uZU&K@^8qQ)VI;(Oh2WGDL9~;UdRWm$58TP}FAm+OD#V&2PGNeK<00r+ zx(m0Loe+dgNB!lO6(q*bR2h3G7tFCP0`Wh;`*)~PGp?7k4lqW3p-tkx24H!fac`Sa zMehY&egL^AL$DP%)vP0wc)Jj?yiyU@0K&Z^Uxgw<0qX$_w0Gz!-7TwCnnbew!wK}@bh0*!RM3jh)+ zuolT*a%ddBO<}$qUs8;+CMLg60<}2yBK5sWeZolz;Wk$}B5Hm3lsVkJd6D$|V@3ek7+So1VE&;E-nxJ90LDq$Zv)Rf`{jQ z1sucNps?|B9|JGVJ z7ec)uSA826rzHq|R-LbDy{Pb^w;WA-R%5Q%x^8ZA?+XBiE3Q@zp)~4>!g{yKsiiVW zgLRz}tZb;5HPfxsUt%@Sl37Uf$2;8UMf?R>?!#A%^V(SdAm3!h)!OxxNcDy z`|OLo;0#Eu4%SUaOlYoXXL1_(F2386Luar=S~YEk!bnO?gP2U0Svp=!i<_nLrf_Lg zn2SfLQ}?h%3;Ama@w#)!jv|t$;&^qUZ_H~y*_Hb;;uLGtOsMD70geEj-z%VQeMmmf zoY(%BXGGgG%c2EU_dcW7W+SqqTashIbx z^s$_MFs&&5HZNK~+7sg~Lu7>_Boj<6gFW@E7j71D%z1_jiiz8!6lp4-#DYv>MgMl5 zf`u~C1yuN384HD}ymOTa`XR(F?rRHb90jRwA`MiYvs-@l!jP>?J@*U)=Lhv1v`g2XjiCKQ*^F`ixk z+ILPUXuWF7Rny^=Crcdf%qxv5u6^icIeN&|^bNy3?u2@BuVz4}$&;6Cxx?I3xyFj9 zX+Pg6G8+I*hTqd$JAVdzhSwu=)@GTu6Ol%H)Jv;{q0XP>6Q1_F=)AjHRz=x}|J3n2 zkXY)AvI)0smXYk?cG2$P3AazlZokJ_yd2e|??rKw)-wTWh-Dsg#19R&XV&SdNqi=M zj&kY+fx|~AqdCPruVo7OElcf4*1>$AGF7RJ!H`V-u6q%Qy{paM?NGh4gxS|e*~}zk zTx`i4dv3!CPyYNP2|D0jgV5UJB$Wcy$!m!{n;8!>cWvp<2qCE$WV5(+yxji~cp>bo>h6s=)$ejBG&&mMFZSer5r*Yf zKRD$Ik{1pi&8cQhT7EN%e(134$X$;xe$&9`aX!-%apyE53-3O6*8wtW+-jD-@#K0I zefHo}^k{SSr+kNOD+-NVTr>Wo)*tH3{&wb z_{3tAEV*ylHzvCy0II%7u*-rZGrTa(_ z-aFSe|MsjLir;Pu0Y%DDCE1GDSrj>X)bt5sU0uAGleVwfF0epNh>%bjR$j$H5!6hB zFIWBKsvL)dl6YUca(_9@?l)pf(U7o&9q~K6Ad-G8vq@qOSCf$+SbS(1oHEyFQM}p% z14~eKVS<}7X+d@&_*w`}7CVi4wr+6hw5GfJ*nxiM1p9*)$M0~)jI-GWYILu*VLmU)i%+=XLje$H|+BcMAZKX!_G@e}2^RO%zVDyeXo+hovLE1@EY*i!R|q+d^G zxC>@D>rJzW7yR@{2MHVem8DB0MPGm=~XR=tul)av#g zjY^G@5$_t8B^&QnvT#rN(C^t~*6D#8t%)KG_XqDgBJ7lZ>~U|+3=1V}bMye+e!?f20Z{6;yCMdO|~ z8O|jLW`8=Duqz7P`)=$&xYN#YDWVdj*sBG8_@&4oUB?A}AvX;%liQ^A@I}lGgrKbd*fd z#f9}JazGMZ2zzUX4)))kxJR*xu181MX!z}CZlFnzcsgKBsJ0Fe%+~2HGI9r z_l+}T=FB@1hoTOjOl1gE_0AF4xTj%ZaAd$tUZOF(+UZp*`TXDXR<=le*SsnnzAu8J zkK0EgY5^yvQ0yQZ;dRFiq_%}+3ugEnbN~8{7GHSl@5f(@u72_~#mlqcOzeyqQMYAd zkcz)umt-*oJZJg#TAE7XmtqPvQ;HyUj43lQiW#asT&??x`*SDyQI!^>rY;oll*2g; zS+{-7*Bfwi^Bx~9s@AnjVaRgY?+Gr5SL1cJeqsZ)lt6rt`7qwTRNNRnd(>2nzAmTu>6%`(fj4Gz9Y&m8;U-{w+$6DRmCe)Lj zJiuP3DCtPQo3fHZQ(qRnBb&IVTC3 n>PVN!u!~{5Xc4Z2`A0Zdc=~+?3Qznu+dF zs$znLb<7v)`jN>6wpO->hnZeGG)9USFD>mlzAkLxQOw*(d&qmHXRIY|H79CODJYtz zm&f9gZDoFS9Oo`s#-cd825X`f<%;wlZqX19t{V`o-g>kxi+sVTPefP3mAy7Mb3A+W z(HaYtiGlxz=yK7lfQM+^CfTv}u_) zHfPsxKU{A5RD-dc^IKVE3sNJW zAMMt>A;nfXagDtEGVQ&lcd(Ms2Qo_iN{j`@@Z+T;0rN@H6+va5SNg>}D2vQFE|sic zwAYO+o=3y)@=LpS3qv4vUKH^X7g6)2d z5g8D>tY@bl)ycY&ly4U1mZj|cD$64XZ5N0cO|ySnZudfMxapF#=5FbRvV7&9mh-Gq zLzjex@HLnls~!HmSqF>8<%gR-%6L~1r(mbAcf7!=3aW+7i?n=DHc-Z>@J^A>2qf<; zYDf&t5__2%W0O>G>-t*FLUou^p7nWkq-3OFqF}ojxh^{8o|T^Evd!4KN}6C=^&LI8 z&#zS%_NSuL)-=OqFmN9x>nWNR)jlL!kvM5kbcNPAgVW@@$ZN$MRsskxzOS*CsQUOb z1+{p7ZBh{0zQ&U$N6&U~+8^9PdzErLoI*`Sq}iRK-XwWT$l0BwU{|5c;|Ak9$$C}k zr82uVaz%-oaxq|wPyi|Siwl7*;kco2SMP6aHcCaudAPeg&v_RFuVy=iB&gw>5=_-? z5~63_z`EM)S!7=MwGW@*f<=+J_Qi{`k5Y7fFqGGHoEEdE;lNV7!N9mthhYnoW#;X$ zpn2bm_L$tU1_k!Zb5(_k+DR`{w9Bqg$l;Xd&fMXUrYQF}Q)8r*?|ieWmL|{sf~MmW zkC3*JiZE0qZOAofGflvpEW7n`As*%Og1fb#HyVeZ`95=WsUlD`97PY3eAXo)mAW|H z6wMbhHqUvpmg-)r(t4MzmDx<;xW%XtZb4{o&#o(__)llY^k7%8}kcMUogXn!m znAOY36@A}`g>a3aoo}1aawIQCTAYsR0mL6hWM$ZT~?w*)v+n=C;5+aRIy5I5h+jMPW&~YwFJjt%~4fM(C zNYQe@*M}^4@q}YAKHSD@>&$v>Bv~ghws$;%qN6%>`AGI9)D5exI51v_$R|ln_w}`H z?f;+P(Zk)zM= + cases.some(({ value1, value2 }) => { + try { + fn(value1, value2); + return false; + } catch { + return true; + } + }); + +for (const [group, suites] of Object.entries(GROUPS)) { + const cases = suites.flatMap((suite) => suite.tests); + const candidates = Object.entries(CANDIDATES); + const skipped = candidates.filter(([, fn]) => throws(fn, cases)); + const title = skipped.length + ? `${group} (${skipped.map(([name]) => name).join(', ')} skipped: throws)` + : group; + + test(title, async ({ bench }) => { + await bench.compare( + ...candidates + .filter((candidate) => !skipped.includes(candidate)) + .map(([name, fn]) => + bench(name, () => { + for (const { value1, value2 } of cases) fn(value1, value2); + }) + ) + ); + }); +} diff --git a/bench/summary-reporter.mjs b/bench/summary-reporter.mjs new file mode 100644 index 0000000..41acfe6 --- /dev/null +++ b/bench/summary-reporter.mjs @@ -0,0 +1,60 @@ +// Vitest reporter for `pnpm bench`: in GitHub Actions, appends the benchmark +// results to the job summary as Markdown tables. Elsewhere it does nothing; +// the default reporter already prints the tables. +import fs from 'node:fs'; +import { createRequire } from 'node:module'; + +const BASELINE = 'published'; +const { version } = createRequire(import.meta.url)( + 'react-fast-compare-published/package.json' +); + +const format = (n, digits = 0) => + n.toLocaleString('en-US', { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }); + +const versus = (task, baseline) => { + if (!baseline || task === baseline) return ''; + const diff = (task.throughput.mean / baseline.throughput.mean - 1) * 100; + return `${diff >= 0 ? '+' : ''}${diff.toFixed(1)}%`; +}; + +const table = (title, { tasks }) => { + const baseline = tasks.find((task) => task.name === BASELINE); + return [ + `#### ${title}`, + '', + `| | ops/s | Mean (µs) | ±RME | Samples | vs. ${BASELINE} |`, + '|---|--:|--:|--:|--:|--:|', + ...tasks.map( + (task) => + `| ${task.name} | ${format(task.throughput.mean)} | ${format(task.latency.mean * 1000, 3)} | ${task.latency.rme.toFixed(2)}% | ${format(task.latency.samplesCount)} | ${versus(task, baseline)} |` + ), + '', + ]; +}; + +export default class SummaryReporter { + sections = []; + + onTestCaseResult(testCase) { + const benchmarks = testCase.benchmarks().filter((b) => b.tasks.length); + for (const benchmark of benchmarks) + this.sections.push(table(testCase.name, benchmark)); + } + + onTestRunEnd() { + const file = process.env.GITHUB_STEP_SUMMARY; + if (!file || !this.sections.length) return; + const report = [ + '### Benchmarks', + '', + `\`${BASELINE}\` is react-fast-compare@${version}. Node ${process.version}; operations per second, higher is better. A report, not a check: CI runners are noisy, so compare locally before drawing conclusions.`, + '', + ...this.sections.flat(), + ].join('\n'); + fs.appendFileSync(file, report + '\n'); + } +} diff --git a/benchmark/index.js b/benchmark/index.js deleted file mode 100644 index d5a411d..0000000 --- a/benchmark/index.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict'; - -const tests = require('../test/fixtures/index.mjs'); -const Benchmark = require('benchmark'); - -const correctnessTests = []; -const genericSuite = new Benchmark.Suite(); -const advancedSuite = new Benchmark.Suite(); - -const equalPackages = { - 'react-fast-compare': require('../index'), - 'fast-deep-equal': require('fast-deep-equal/es6/react'), - 'lodash.isEqual': require('lodash').isEqual, - 'nano-equal': require('nano-equal'), - 'shallow-equal-fuzzy': require('shallow-equal-fuzzy'), -}; - -const advancedPkgs = new Set([ - 'react-fast-compare', - 'fast-deep-equal', - 'lodash.isEqual', -]); - -for (const equalName in equalPackages) { - const equalFunc = equalPackages[equalName]; - - genericSuite.add(equalName, function () { - for (const testSuite of tests.generic) { - for (const test of testSuite.tests) { - try { - equalFunc(test.value1, test.value2); - } catch { - // swallow errors during benchmarking. they are reported in the test section - } - } - } - }); - - if (advancedPkgs.has(equalName)) { - advancedSuite.add(equalName, function () { - for (const testSuite of tests.all) { - for (const test of testSuite.tests) { - try { - equalFunc(test.value1, test.value2); - } catch { - // swallow errors during benchmarking. they are reported in the test section - } - } - } - }); - - correctnessTests.push(() => console.log(equalName)); - for (const testSuite of tests.all) { - for (const test of testSuite.tests) { - correctnessTests.push(() => { - try { - if (equalFunc(test.value1, test.value2) !== test.equal) { - console.error( - '- different result:', - equalName, - testSuite.description, - test.description - ); - } - } catch (error) { - console.error( - '- error:', - testSuite.description, - test.description, - error.message - ); - } - }); - } - } - } -} - -const chartData = {}; - -console.log('\n--- speed tests: generic usage ---\n'); - -genericSuite - .on('cycle', (event) => console.log(String(event.target))) - .on('complete', function () { - console.log(' fastest: ' + this.filter('fastest').map('name')); - chartData.categories = this.map((test) => test.name); - chartData.genericTestData = this.map((test) => ({ - x: test.name, - y: test.hz, - })); - }) - .run({ async: false }); - -console.log('\n--- speed tests: generic and react ---\n'); - -advancedSuite - .on('cycle', (event) => console.log(String(event.target))) - .on('complete', function () { - console.log(' fastest: ' + this.filter('fastest').map('name')); - chartData.reactAndGenericTestData = this.map((test) => ({ - x: test.name, - y: test.hz, - })); - }) - .run({ async: false }); - -// **Note**: `lodash.isEqual` gets different results for Sets, Maps -// because it **is** correct and `fast-deep-equal` is not. -// See: https://github.com/FormidableLabs/react-fast-compare/issues/50 -console.log('\n--- correctness tests: generic and react ---\n'); - -correctnessTests.forEach((test) => test()); - -console.log(JSON.stringify(chartData, null, 2)); diff --git a/package.json b/package.json index 19412df..385d241 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "typings": "index.d.ts", "scripts": { "check": "pnpm format && pnpm lint && pnpm lint:gha && pnpm types:check && pnpm test && pnpm pack:check", - "benchmark": "node benchmark", "format": "oxfmt --check", "format:fix": "oxfmt", "lint": "oxlint --report-unused-disable-directives-severity=error", @@ -16,7 +15,8 @@ "test": "vitest run", "changeset": "changeset", "pack:check": "node scripts/pack-check.mjs", - "size": "node scripts/size.mjs" + "size": "node scripts/size.mjs", + "bench": "vitest bench --run --project 'node (bench)' --reporter=default --reporter=./bench/summary-reporter.mjs" }, "repository": { "type": "git", @@ -44,11 +44,8 @@ "@types/react": "^19.3.0", "@vitest/browser-playwright": "^5.0.5", "@vitest/coverage-v8": "^5.0.5", - "benchmark": "^2.1.4", "fast-deep-equal": "3.1.3", "jsdom": "^30.1.3", - "lodash": "^4.17.10", - "nano-equal": "^2.0.2", "oxfmt": "0.75.0", "oxlint": "1.90.0", "playwright": "^1.64.1", @@ -56,7 +53,7 @@ "publint": "0.3.25", "react": "^19.3.1", "react-dom": "^19.3.1", - "shallow-equal-fuzzy": "0.0.2", + "react-fast-compare-published": "npm:react-fast-compare@3.2.2", "terser": "5.51.2", "typescript": "7.0.2", "vitest": "^5.0.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c5eb1c..ca02420 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,21 +184,12 @@ importers: '@vitest/coverage-v8': specifier: ^5.0.5 version: 5.0.5(@vitest/browser@5.0.5)(vitest@5.0.5) - benchmark: - specifier: ^2.1.4 - version: 2.1.4 fast-deep-equal: specifier: 3.1.3 version: 3.1.3 jsdom: specifier: ^30.1.3 version: 30.1.3 - lodash: - specifier: ^4.17.10 - version: 4.17.21 - nano-equal: - specifier: ^2.0.2 - version: 2.0.2 oxfmt: specifier: 0.75.0 version: 0.75.0 @@ -220,9 +211,9 @@ importers: react-dom: specifier: ^19.3.1 version: 19.3.1(react@19.3.1) - shallow-equal-fuzzy: - specifier: 0.0.2 - version: 0.0.2 + react-fast-compare-published: + specifier: npm:react-fast-compare@3.2.2 + version: react-fast-compare@3.2.2 terser: specifier: 5.51.2 version: 5.51.2 @@ -1026,9 +1017,6 @@ packages: ast-v8-to-istanbul@1.0.7: resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==} - benchmark@2.1.4: - resolution: {integrity: sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ==} - bidi-js@1.1.0: resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} @@ -1292,9 +1280,6 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - lru-cache@11.5.3: resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} engines: {node: 20 || >=22} @@ -1330,9 +1315,6 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nano-equal@2.0.2: - resolution: {integrity: sha512-RtAioZtSSppEWRruYU8oz2AKPJWTW74TpzcqP79n7Kh4nHW+gmrJGcTuELmid9tvGR4Xp2cCoTtYIIDy9iWO4g==} - nanoid@3.3.20: resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1404,9 +1386,6 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} - platform@1.3.5: - resolution: {integrity: sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==} - playwright-core@1.64.1: resolution: {integrity: sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==} engines: {node: '>=20'} @@ -1447,6 +1426,9 @@ packages: peerDependencies: react: ^19.3.1 + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + react@19.3.1: resolution: {integrity: sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==} engines: {node: '>=0.10.0'} @@ -1480,10 +1462,6 @@ packages: engines: {node: '>=10'} hasBin: true - shallow-equal-fuzzy@0.0.2: - resolution: {integrity: sha512-HKGvWaQ6fuFhNH2k1krxxmhKYxyPSbPzwCkM94qYVoLKCGCqg0r9bdCoX4A37UJvAfyLje9zHS5F/SsrYgrqgA==} - engines: {node: '>=0.12.0'} - shell-quote@1.12.0: resolution: {integrity: sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==} engines: {node: '>= 0.4'} @@ -2359,11 +2337,6 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 - benchmark@2.1.4: - dependencies: - lodash: 4.17.21 - platform: 1.3.5 - bidi-js@1.1.0: dependencies: require-from-string: 2.0.2 @@ -2584,8 +2557,6 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 - lodash@4.17.21: {} - lru-cache@11.5.3: {} magic-string@1.4.5: @@ -2623,8 +2594,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nano-equal@2.0.2: {} - nanoid@3.3.20: {} node-emoji@2.2.0: @@ -2704,8 +2673,6 @@ snapshots: picomatch@4.0.7: {} - platform@1.3.5: {} - playwright-core@1.64.1: {} playwright@1.64.1: @@ -2736,6 +2703,8 @@ snapshots: react: 19.3.1 scheduler: 0.28.0 + react-fast-compare@3.2.2: {} + react@19.3.1: {} require-directory@2.1.1: {} @@ -2775,8 +2744,6 @@ snapshots: semver@7.8.5: {} - shallow-equal-fuzzy@0.0.2: {} - shell-quote@1.12.0: {} sirv@3.0.2: diff --git a/vitest.config.mts b/vitest.config.mts index 5ab4128..e7a253a 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -61,6 +61,12 @@ export default defineConfig({ }, }, ], + // `pnpm bench`: `bench/*.bench.mjs` run only under `vitest bench`, which + // adds a " (bench)" project for each project above. The script + // picks `node (bench)`. + benchmark: { + include: ['bench/*.bench.mjs'], + }, // `pnpm test --project node --coverage` (CI's Coverage step). v8 coverage // only works in Node and Chromium, so the browser project doesn't run it. coverage: { From 1c2cd2c883fc6708a19f5c10b5b43b30921cd29b Mon Sep 17 00:00:00 2001 From: Ryan Roemer Date: Mon, 9 Nov 2026 10:28:10 -0800 Subject: [PATCH 2/2] Make bench script windows friendly --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 385d241..bcec8e8 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "changeset": "changeset", "pack:check": "node scripts/pack-check.mjs", "size": "node scripts/size.mjs", - "bench": "vitest bench --run --project 'node (bench)' --reporter=default --reporter=./bench/summary-reporter.mjs" + "bench": "vitest bench --run --project \"node (bench)\" --reporter=default --reporter=./bench/summary-reporter.mjs" }, "repository": { "type": "git",