From aeb75a5f8e321ac49b1ab1e544b31c75c7905b40 Mon Sep 17 00:00:00 2001 From: Jackson Hoffart Date: Fri, 13 Dec 2024 19:06:49 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20gains=20`sum=5Fwith=5Fthreads`=20(#?= =?UTF-8?q?19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove boilerplate * add failing test * gains `sum_with_threads_impl` * add more tests * handle empty vector case * remove superfluous comments * add R wrapper around implementation * add savvy-test feature * update function name * savvy::savvy_update() * devtools::document() * add benchmarking to README * add an R test --- NAMESPACE | 4 +- R/000-wrappers.R | 65 ++-------- README.Rmd | 47 ++++++- README.md | 59 ++++++++- man/blazr-package.Rd | 2 + man/figures/README-benchmark-1.png | Bin 0 -> 30695 bytes man/int_times_int.Rd | 19 --- man/sum_with_threads.Rd | 19 +++ man/to_upper.Rd | 17 --- src/init.c | 36 +----- src/rust/Cargo.lock | 16 +-- src/rust/Cargo.toml | 3 + src/rust/api.h | 9 +- src/rust/src/lib.rs | 163 ++++++++++++------------- tests/testthat/test-add.R | 3 - tests/testthat/test-sum_with_threads.R | 6 + 16 files changed, 227 insertions(+), 241 deletions(-) create mode 100644 man/figures/README-benchmark-1.png delete mode 100644 man/int_times_int.Rd create mode 100644 man/sum_with_threads.Rd delete mode 100644 man/to_upper.Rd delete mode 100644 tests/testthat/test-add.R create mode 100644 tests/testthat/test-sum_with_threads.R diff --git a/NAMESPACE b/NAMESPACE index 54fb81a..c3eeb37 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,7 +2,5 @@ S3method("$<-",savvy_blazr__sealed) S3method("[[<-",savvy_blazr__sealed) -S3method(print,Person__bundle) -export(int_times_int) -export(to_upper) +export(sum_with_threads) useDynLib(blazr, .registration = TRUE) diff --git a/R/000-wrappers.R b/R/000-wrappers.R index 1cad530..3c12ace 100644 --- a/R/000-wrappers.R +++ b/R/000-wrappers.R @@ -37,67 +37,16 @@ NULL stop(class, " cannot be modified", call. = FALSE) } -#' Convert Input To Upper-Case +#' Calculate the sum of a vector of integers using multiple threads. #' -#' @param x A character vector. -#' @returns A character vector with upper case version of the input. -#' @export -`to_upper` <- function(`x`) { - .Call(savvy_to_upper__impl, `x`) -} - -#' Multiply Input By Another Input +#' @param x A vector of integers to sum over. +#' @param n The number of threads used to compute this calculation (int). +#' +#' @return The sum of all elements of the input vector. #' -#' @param x An integer vector. -#' @param y An integer to multiply. -#' @returns An integer vector with values multiplied by `y`. #' @export -`int_times_int` <- function(`x`, `y`) { - .Call(savvy_int_times_int__impl, `x`, `y`) +`sum_with_threads` <- function(`x`, `n`) { + .Call(savvy_sum_with_threads__impl, `x`, `n`) } -### wrapper functions for Person - -`Person_set_name` <- function(self) { - function(`name`) { - invisible(.Call(savvy_Person_set_name__impl, `self`, `name`)) - } -} - -`Person_name` <- function(self) { - function() { - .Call(savvy_Person_name__impl, `self`) - } -} - -`.savvy_wrap_Person` <- function(ptr) { - e <- new.env(parent = emptyenv()) - e$.ptr <- ptr - e$`set_name` <- `Person_set_name`(ptr) - e$`name` <- `Person_name`(ptr) - - class(e) <- c("Person", "savvy_blazr__sealed") - e -} - - -`Person` <- new.env(parent = emptyenv()) - -### associated functions for Person - -`Person`$`new` <- function() { - .savvy_wrap_Person(.Call(savvy_Person_new__impl)) -} - -`Person`$`associated_function` <- function() { - .Call(savvy_Person_associated_function__impl) -} - - -class(`Person`) <- c("Person__bundle", "savvy_blazr__sealed") - -#' @export -`print.Person__bundle` <- function(x, ...) { - cat('Person') -} diff --git a/README.Rmd b/README.Rmd index 32224cf..b1ea87c 100644 --- a/README.Rmd +++ b/README.Rmd @@ -36,10 +36,53 @@ pak::pak("r-staceans/blazr") ## Example -This is just a dummy example: +Here's a simple example, computing the sum of a vector of integers, using multiple threads: ```{r example} library(blazr) -blazr::to_upper("hello, world") +create_int_vector <- function(n) { + set.seed(42) + sample.int(100, n, replace = TRUE) +} + +n <- 1e8 + +x <- create_int_vector(n) + +blazr::sum_with_threads( + x, + n = 2L +) +``` + +## Benchmarking + +When running this sum against very large numbers, we can see the performance benefits of using multiple threads: + +```{r benchmark} +library(bench) +library(ggplot2) + +results <- bench::press( + size = c(1e7, 1e8, 1e9), + { + x = create_int_vector(n) + bench::mark( + single_thread = { + blazr::sum_with_threads( + x, + n = 1L + ) + }, + multi_thread = { + blazr::sum_with_threads( + x, + n = 4L + ) + } + ) + }) + +ggplot2::autoplot(results) ``` diff --git a/README.md b/README.md index 3c430a0..f81905f 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,64 @@ pak::pak("r-staceans/blazr") ## Example -This is just a dummy example: +Here’s a simple example, computing the sum of a vector of integers, +using multiple threads: ``` r library(blazr) -blazr::to_upper("hello, world") -#> [1] "HELLO, WORLD" +create_int_vector <- function(n) { + set.seed(42) + sample.int(100, n, replace = TRUE) +} + +n <- 1e8 + +x <- create_int_vector(n) + +blazr::sum_with_threads( + x, + n = 2L +) +#> [1] 754832969 ``` + +## Benchmarking + +When running this sum against very large numbers, we can see the +performance benefits of using multiple threads: + +``` r +library(bench) +library(ggplot2) + +results <- bench::press( + size = c(1e7, 1e8, 1e9), + { + x = create_int_vector(n) + bench::mark( + single_thread = { + blazr::sum_with_threads( + x, + n = 1L + ) + }, + multi_thread = { + blazr::sum_with_threads( + x, + n = 4L + ) + } + ) + }) +#> Running with: +#> size +#> 1 10000000 +#> 2 100000000 +#> 3 1000000000 + +ggplot2::autoplot(results) +#> Loading required namespace: tidyr +``` + + diff --git a/man/blazr-package.Rd b/man/blazr-package.Rd index d20cc1e..48f2e16 100644 --- a/man/blazr-package.Rd +++ b/man/blazr-package.Rd @@ -6,6 +6,8 @@ \alias{blazr-package} \title{blazr: What the Package Does (One Line, Title Case)} \description{ +\if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} + What the package does (one paragraph). } \seealso{ diff --git a/man/figures/README-benchmark-1.png b/man/figures/README-benchmark-1.png new file mode 100644 index 0000000000000000000000000000000000000000..fd3b1bb5993615c0920298eb76990d40474a64ce GIT binary patch literal 30695 zcmeFZWm}Zr`#lUpgCHQGbV-PSBAtVPl!T;!fJ(P?m#82p(p}QsJ)}z4&>hm6lQc5sAcAm$EYS z_zq@dgyLOshnHC>_q`imxiT!ZoV*`qQ6?7LzA=?x%J^vhBF&UVgh&j(DQh-t_FB7% ziPkK4GmtH%OeqD`m-z&KCiS(S6l=ZuQ7J5}%Dk>~@^mSF+jDu^ZfhDRxaz)kwWOauvWXXUfdbxOAL<8Lh8q(YjVyT?}KdQ4AMByW1pzw@((2~X>u|4G#n zi4nwOi_O?nUw>@yUI6QOI zNQl9l3*{%pA^UWfL%n?WahsP$Ia3afihB0q?u zKet1h-nncLKX`ddBADi;sk`>e)J$x$6_c+k=_X%KhGsWrzOkHMAyulHUA3*R^gVc- zaZSGi&-u1(qDEhP!_&ni*8z@Y^oD-$p$vyisP72{UD(2dJG7}Zp-k;62A>T21Z45J zTO;Z04EC%023?Tpm6Lb_YXeuD#9FS54WehudYfgFc=1PjM|;gXB1Wuq1w5=Y0%1?o zlVHPktM&}WWM*vkZR3Lpw!PQi^p;18^p<(GZd_q_qIj)Os8@2Yb9Jd>d(>EK|B%`j zRxl7rXH&CyRhpWj$*q0{q3%TTsCI^78l!7pEF8CshMKn@hf*_o%}+0g{OqwyvQ4s? z@ov+ZWw+I{)w7|tOOi4hblK)I-zrQk#$GawdN!H#SZkC`h}q!1@VD>Z4CEA7h(El; zdd1=D$eVlXGpP;?y|u{NAlD<$n>5>Q!z!w~1TITAKbiyUw&@Ks`?}AD2 zSgV5n=4>7c{M@9I$J=lp~&kni^<4MnovTPd@f@oFOPH&{Evx*>}QIE3jRX|e`Mef z;Y%QYjzE%=QU3goJd5~}P;X)q2?>fMBO#{hjJz?05l^&u+PG_cMCK$VB@v5_{g#H! z`)$B$$@_hy@E=wd#VuJ9`W#-Lr+y^fzC~a0JnUubTQt_!AG2Pj9y?xET+A#tYaVSm zZdZ)VtYcU^IW8p}U-nE+mTsT-2r4u((9(J%K`?24eFQTiE5;sux59@aA*25OD1x9S z$7BBYJvhmfmmB`aj zsP^)!P)BhdS}-Kq+hYf|muH7#suUfeeh@=L!~Ef#oE+J;^>u9>zncWoHi-Yc5%Y6o2>h+^fhxtHBW<6+n2jdKSpSS!3GvowZY$Y*e~tv-0T*>u zm1_JqEzsnbXqZr(BY0-RTw(oLj;e-6rbX)`qb~fU-l#WMxVX4`Td?hpJk8R#1Nf^Y z$J1_w%?(4j8cemKoYF~+*H_1)F}&8g9gbO#FjlI=K9!jb-E&!t&{&AGt~U)$NSt?N z?@17>Y`2|W2!iGmzSr|OjJVkOep3dyZ(p%jT$oxstVdyI+`O>8KV43obf3&83ydc< z<8`wH+QDlz6_qd(o>!G#OEJa^HR}bp3P;F~_Dx8Khle`~y)MmN_Pco(L!_ue+SgwT9I|JTU=Jf4e zrs$=NRo$U_%p>EVc*k+ml9x@8z(eq@hUb3V3~Fz0l%6O5rT@mmWwdAvuH~Dr9Mxa8 zhu-D6y)l#kK>251^x_)a=$(T{x z*tgBf8P6l3{(YnGA8_Td^OoZilVZOm@Wv)p%sQ65MS#Kc6kII~>yJ5)NynoLEr>CS9Z z)Bb+dd{C)aqc<|j+lf6o80vwLcJb@?7RAjZ%Fmv?!XfQ0SJJrQG%Zm>vUYa1xct?R zs${vg{-*rT9@Aqx;lf^#bBoh;z3Vmhl93iLV(kq1I7X^3t)4$!Hi}y}qNpzSQuTT; zaLE|_C>^6I?7AM7OjyBWOifMSxarcsBf;*_e(X+F>U|}qQ|nw@hsmy8@zkp7C;Dn% zs-I=)H^DS7q4$SdL z+sVX-iy1zvgc9g+JjHuJA0zBmVHwiT^uqn}aO8-*wqTjDj)s?-a0g6p2QabK0y_F9 zpMp6WF8a5=>tsz;jX!qXt~;&?Jc%)coMGr)w;gYc4P5QcxUT0f-t;>0y|~==s$5=V zhi%YRPC3T0l_%=WcwN_7G+Z8~EL)}RKj~Ewj-vLu*d-|+N}f##WG|Tt=cqq<9oBoj z7uC>)!%_RvSCMVxa72&8AQf5Gq_ZyZdb~u$enklubhTwNo6?|irsao!U$1@= z9ZojrxHZWiP0CRs2ZfDaA5}Jtna4N=CfRnd9G#`)Pvq<19FLoGe6&Zbky-a4S$5Kl z!-&q&_?nr7K;~5MVPFF~?5j+J>MDO$Qzl~_iOBWE{!wRrV(S)I9l8R&$%8-<#ar0K z3l7@AHCbZqx;%xPwW_ven#!f<-TWz}l4L)7VvA!*HUd!smNZU@f%F#+{kAsj+BcbJZyNz?{<chujkDi#^+* zJNFxhF!Ttk0DFb?U?O4asbP;g60`n&Ir5v2wa=&QlnO`4OxCJJZ002Te$okus1_!m zCKvFDrV`K~eSY%MA%z6)Ee|X5I_=aOCy}=_wY}!!?UjJplyOU45_FPPIs zJ*3Mc<)50~tT9WJN;|IETu$<07vPbBjdPn8Wh3XKSC5;e^*$a6%&SN?E6S6By(&lR zkavBBw|O*j$gQ{ONoQEtxYCzu3D2B&;m#W1p!(X{$5vgjw{n`H&s!c>Uk;4-J6yEQ ziYouoeLcT&gMYE2!VEvp>wp_+XkQ_0df}_sqPn&v@kYw=`K5fxkezbxGs~u-X=L2z z<%hc34sOR=Y=Yahc43DuVU&<@*3{)F&uS`BPxr9t2sj~61IZHsYp#>0OXW(vF2Xk* zE=s2DR};f(Zd#sxvS$-IT?ktQJ2C`=%DEAb4gQMutKOvedW$(K9TvX#lh9W#lMn$C z1#JiWi{K!`fjANSPf8~Jjmp+}aIvkg^B=h0z-_Fp`n`Fk)0!3*+QWSHmCDmqub;!j z-Z{ptIoD1(%?MWoT20l+tR!rXuMGmjo?Rw`uRUjS%6qwkqH7T09aK3GX|~Ce#BZ0e z@?4h}(;g>>ZJ(?$!ELMhNbVy{sNZGYlWJX2csaJRf1Q;5xOPAnvN=s5;$GD~Zuz;N zmTqhNLt>Z~6J>ph-qr!du$RD#h3_{-8IV37Ca0NtScVKHD;Ph4b9@^g&(c}{%FjywxJSOXbu>+g_P&ZQlXlS?^bo3u8Fw$h#JpA>}xIGoQ^KaiZK- zd`v@vy!G~rWT3+Y5!`zd#Kkh+OA3#JK3ljL2HJMKJ@3_AAcw`v9NxosL3#s$Nfmgw z6nyZY)`4P20V~-sbe!%?WSf1QW0n0O>&5Zc+4|=PA2P_TYm3`?UF%+20ly~CGZL8= zrR&;Wfw>}5t_Jl*;Wc%cEOy!A^fXHHb~K*xJm0M7)PfC{cM99r+_wBUsBTf`^Lnz% zrU|>uae9TvRYm2#>4=U~xlW+~iCV8XB~xa;&ey5+@%)0RSgZb*Vr~SJu+_uBc+Z0C zvb^!dY#9_aTb`{AQf9ClOdC~K+ zUW+dwcxwHe>pKL(9Neq&vEy-RO_}H;XU)O~6nNc!BbX9|$!J`mH#+AD;eo-P>mvnp zl&u+FWruVWvGx=Rj;AZCv{o9aL3jHjDJ}E+lU!d-j8F2VbaI=Hq&kYN4N~_pQnse) z5i9h=sl zw_8?>wOsAmYuj@{hqjJ06J0wJskO*AQ%1<2&?WE+sB`tpONVfky_e~w?N-t5KaB5( zGH}VqaNa%0_sE||=Y|dg)50E_A(kIcDdcT1&CPdgG7nDX-3}>Hnf0d+DykE@K4}%* zh%%{TH!E7skREwlC&SY35*A?oRablJA>3;0fGqFPyOk=Hx`Op6zWgtYIt9YmH99M& zt5%(gs3`<*x}b~l^bx6JZyoySl6Cy5;}6REx9(h`P?-0N<_U-lYf2Tq#@dEK%e5cF zGlS9xXsL_wSo#FRzF*>*)*U!8Q1>W0t%xzeUNn%H)g5SMeRrG`?4TbiKizZgjaxdc z+MegnJj99D#rTFR*uQR2wYYU^XhT%liW3k8fEAfXA0{;1cQMif|W7H;8{6WL3VSnJ9p4DgTM7*=<^4 zTtjHN%(Ji0>nGtz`I|lk6PI3>!YBy+?e%@M(Jtdr*0k@ugUkw_;NVl9?G8{HjP$r z2yb5ubC_OniC)>Yp|oejs1tuq{Vv#~zt;X9g$JiHoy>C5;Bw143MFB?T;_H^tkgj( z+d0gt@TMQ8$J+C>+LBYP=)4*8%B^;RVJlCwyb&>v8#XB{cSfk#e0wmHm*D}#tc3TJ zIXhBE`064Vv?AZ#Q*I<5K+|i)NXnuAQ~$-_(;#)f6F8UnQ`%ua_eLu7zA2(6_2&pD zEfgsh%i-wbpMs942qcHd)b2S!e~uVD_D15mwYG8VPj$uo5|mC5krP$QKS$c$fr}Ui z$C&=Cq*5cI-g4i`7Wi`{7zsJBjLRUG~tp}7SvicvC7`m@$hWE^~N zlAWf^wZBIgfq^{w+9N{nUxDfaA)!HHoAp&3`g`OlCcd}P;{*@7KhuDAABv>)e-q>L ze-neSwEwf1-}?gk|JGJY=DQ_mn1o3b3r#M523Zm%{LgXt<9pkZ&;-Bs2|b=B-rm0^ zekf+*_C+ek(<@tAQZnFJ_6Ya}s3#WR`mpS&C>2p}|J}L6)3lWzdV$unj;CtvkQ`i{ z9W?u*Y_l`8fF(~B!)s6Vv;u`6FCH|nM|4bBk(ioDd%YT^hqM!B6@-?wX2_fhJIQ8y zR%63!lJ0BmeWX#+{d;l4Nu;}9N|psydZCATGoBfZ1WX|NcfNg zEBY%#k0NsBrLv)teHIp$PdJuIu5r~m3&=+36&+eole0+}uiuiGP{8y9&)l9m)jA=~ zM`|?|(J-OLbLaXt85cD&V!q+Az^q?HgS39nR-|!lC^G6x)W^MNdT8Gg-6uf zCGXRs!zledtB)MV+Dd58X@gm9S+7lAy4r=0rJP{Lx-=g(Aa1%LXDX#$#A8>C#t`CD zpp(87K45v}S@*HD?CIL^s9f#$CutT*2Ju=x85$vjTY-}V%+fw*^cm94Kcxa1GPHQ2 z12e+IiPBIadTt_LC%Vb?30lj%ygd6%eD5CNcJSA^2~TNH)nF}yeDA_*o|2!~cJ%!@ z5&gv))~0VBOT9C)EM|F*3c?HKgZoctVjv2YR&(~NbR~|KoTYa;Q2RMvws@Tt)+mq} z)zeZ9Y;H}YWS*eoDtPga>c7G+GiSP*_TbS~9LZyO>MNAZjck%gA)Vs`qFVzgd*(Hum=e+BQ=ZC52 zzk@8-EgB?C^l#dzoU}@AUAuFr5JG0wFsRG>Qw?@CQl0z#8zFu%%;f(i6*Oaz$#>xv z&-TP=sF3$9=5a?Zp9L&==u=f}-qLHhqBGo_YyW78-&W;aYMXwiZ9}?I2>H`HC=Vu| zdACZ~ft+Vl!I%<7$4g-E{J8jSB*^>z;xmn#CFxw@C_bZTYwKxq9LHnE4sxj)Kjx)} zs0;8}SUw7KR6uF&K$brA>|tErlMEyo<2a5@7iN-^3kwl+VW5GMxwj{gU~5Rwsa?MXBSwCTCDu@{G-jGMv={b9Wztm6B-)O9s+slR(ngK>;F0$k zB5RYu$FyIenHHOnOEwwEM?-!TC_X<+`w3b8Er;e0+LuPeyLeM#2SWBLjkP<+tE{iB zE*K2e!B#E3+Cb!DZR9>REK+fID9&UWCzVJ|Mij_ewip~*sgigPQ|YRil$=IG9Mwa- zO>u`zx5%Wvgv1VbhPe^9e~9Avb$oA{J{hBK-ngI zf997~JomJ!8hfr?dQOi5%m4kA#RO^B;PqJ4mWZ>7!1&zgk+U1rBDkjlkl zfY)slIlgubJ#0JOt>8$2e{>o{h-f!=B4cpYg zV_sgmW`nAA?x9cS)xiE}sr|?B`h&+ca^J4N`bmLI)9#@0*~TRC8}&T=)@^GoBq6X9&qTdY*41EABYheHt?cGhXKrmx z=G9F+m#4>+sP%~47fXa&ZPKgIKV~f`{1UAGnAay}+W$^}hs_ncTpjn7rrY}c0J(s0 zu7+Ph`}%!Qi*@&-h!_7Rrl2%$y<=(=zCC3xRE)wYV+|}K2rQKWgN_SAHx&ENiSeMt zhZ1HdacM$~?%SG}-G-&c^cSdr1vAcuK0$#4!a*~qn`X|rzaxUxaH%V1*Me*^qrQH+ z{_0}CcM)(gSnP_6IFyxplXkryuNeNL%AoF$&+|?pH7pH8dS9sKCTjf#DV>W3ujh77 zIQ4VOaJVklf|sTm_BvQO-A~?R=jDBJ>BS+rAMACtpH#A50Bfnux7z8GqHYI3GfmVB z+mBt*55;R7H}9dK-|%NGe9p5MmrZ9?yZeNH)*I!ADbF33`=>=#j@Te+k%383N7C&- z|K`0)m&1Sg6w7`)G2wl>D&`N`Sr?BFD-B0WIPUXD;^|jA^vy52=Gs2WuMx6L!T8gIn6;gk~BA-0M%mTbv^q#Tg|67QRguUOqGLy$W)4(moy zYB8ty9wJHZ*O$j^T9V}5L^z@sJHD6hbuQ>ORm0*s6@XaLtgz6GlM?fVI*EQ|E7C%r z`r^5j;lP*-=QKZ^N@L3eK$K;mt=TDQ?6M1QVW!o{%cXk*i$6QImO6VWNKLY$j;@`` zi9D_oJcePJEl&w-*FUFxnP#93z6nX^5aEyksBBJ_Pz#!H8 zbYkLatux)^O31CA-=SQ2cX{GRsq-_Q$Vkp+=HN&Ycqcoo3;hjccPe@myUxAJN!vgK zXJY#ei>Zy$X_6LDJLR2RJL~YTzFkS8dIdFW_wES0mem42!}dEauk`iRrDbF0gy@;c z0M7IWT=E5A6gSaJCM-%nzi^szAke<;;5Ck0(6p%kMcAmlOi)kzF}MF9&29l~EA(h{ zqO-V_iZym-vf6I(Y`u_o;4^negodtZ=l+m}UVDsjBFp)9LxcPAl#?w0vg5eXq}Hgf+I%lOE& z(=ywINUvm%?V-NQpg>G^Ld>V<>xm`-?c2?jKQ>h3gE=(M+&7M_yMpIv#W!3cvNWGA zepGT9k^HPBcUIjxX&7tLKcCw$;o8H%TJ0dc7H7xVLtMCCqwH45*V zUnkMpA8a6Pzo%akRIXibpqm@Em^p%6S0G_{)MCCwnygAuH-DL#HZs_up&L5)lRl;M z`_zN3-slcOBR7(}jKL>*D?CSqO+Nt>#|l`)hHez>@v$TUPZbn6JWc<>_3jt1QJT%4 ze$*Anb49PdFT|P`gos|QDwa$+PPCgB)-jTG@hFnwty|25M^FYbSq%< zMQ_dRR=r8)t&k|BY1Es@rjqfrSZBSa5AdQEuu}(r@8O?E6V_2pz8IYcs{@N*ADJkl zSyB#pkIHLBf>CvQ#K9FWCBH1uQLif;JkBOfGhnEbW`un*%QAe*m91AM_T2Y$FGcTV zER&*#_=7JJ>oK3^SBIs*Vy%q~3*=jFdoH1qw_98~?LQqHU`xIFwD{c5ige3?voq7> z?g0C=KG}n1@r0`$Kli2*2VQac4xu3D6imiAZAq0#1Il@Wi!CmWhI3eW9w+1Zb8}DE z1a@q@O6UEWvO^I~Pc6nv($6uc9rU`7n%wsn9(p}0SLj>lbDw}87l+@q7u02vqTwfy zU^VQs<`>+mN~Q2T`N?TD$rtnB)!Q4C0%gr4WWi=QUo+}G&d0qKVAbVl&wLf-$?!kG zUYfqlSp__Dx#&oqCVgSUmFo|r!GggJFTm_!qGOSDnXzWX9Xt|##;u3@)L~-#{9 zNZKeJzBffi(aVl!yc)XVczYe2%~`V?56wH9lf`^D&a9Nym-0mgV~p>avznS{l^DpZ zOjPMPt@bxZ3U1x_W-wzk903E`J2LCkrY4ekr|UNTb~5_|k&6N_z z#<}_FoL$|9Lrd-)oTN`J(LV7MN}tQM><$WXWj`~|Pj9-pjJu-C zC+)Ols~pyUe`h-Q)9n3W{V8=#hZnGl9(KL@+I+Q%ix>O-iKLe)vC{p}G}xUldCMmPLlX$~tA5cgl3(FW+cCsdLSJAJ-TidoxVbSzF5yUV*{XJ!OQD zO|Cal_*Q#>pk)-cJ<&3*JGO1J3XX0)#V z_q&@V!{o7(L~8r>u#(d|<8|p?pB`Ulz4z?w4?OX~pt@~wInY?@v^`y+OZLFrskXv@ z8nCTAPL`7WqV=vJ{Q@hZ$J%ynTP`sekSn&z&>C^P2uDRxma70+8+TpP+m)rxeqXwL z$Dg60N|=4&R}gq{g@F}~MEA15P+5y{$T*SGQRGQ2zO-uNj6P1b@R^3P$Y#4w%FPirT7 zo~<(ae3h#Nx`a01WjUlMY~FS2ZCi{)4zpEG_UoiKD_Y8YE|+}LfQQ$gHZx14u5I56Ai7nT2@DQ_qF#SXM7JjcRpMPbqKTzY_7- z%NSQ>u!Bs$qE9nhznasmP=#b@G|iN1sJ>@O-2YguX_b#!6HA^TAIQRTj*56&)+Lei z4SJ>o`_Kfd?4B2C2k5CLVbd?tcz0H+lq_`y)5I)H`jHMk^G*kU#}j-i;Iw4{+XIAi zAzV$XcG_{maxo*$ijb0HoYg?qkA#&+J6ZopVbvUV43A|jF2fh0J!v!iJNl zNMuhl$m=huuN9w?Mntnyv$&l~W()6`>;_Q~zj#lI`GYlCZ^`48s|@|3hD~dJ1p&17 zbH>mh$Mn^Nbbd_z$8a4;(5l(+DVA-xX`MDazpb?SS-R!!)84q}E$2@- z?V@1%(9{@`o+-xuUW3m#gXXD0$V0oEyWo>^?NHf|aCL{7S1o$AMQF+Wp_kI6WJO&4 z+2sCRc>3aP#VOrh;Z53sna{(@JbE}`hi;q0qMEJa-5evW-Zh7q$B+8Uix#FT)2B)m zdB-G@B2|qUvyOw#G{Y7@AzOY6IpUR%BUGt!I|ed5c6;)K2fDD4!zXDPdLC*)f?IqO zIk(xVFBcucu55-5zo(02n45t7ktgdj)5)OFl<3^o)AiAwVMy1o2o8c^1C;_h7T*$M z6ns{tvp`>hoB8lP7St}=DxA9#HJ~fpzdKCfv&O;vF&hNFw7x4+2QkHg&gVxes24iL ztjj+o)pdTg6LeCsH}FykHvj!@9c$Dt2qBol2kQ1{PNrtZD)^-EB8U9S;+0vIj9c8)uU6HI z2RLJN(4KBbObg9z4+)kFGCHBgVsUL0M}@~=ZJ=Zj$hLabG?{$ga^unttBfIOJ450c zfNoIxgyU50PsVU)Goac&YqU0E&8|7&WN|xUIdA*XLXi4Hkda@}u2b@6N4yyFqbDxk z%Xy{8?0TcgL)86yC%I>edKoJBOqKbiox~Mx&I^)v zA)TJl2g$Z3j$uG#Na2DJ>p}b?tEZu~kcT+Y?8mN;m>#jfK=D_4exg((L}KE#8S$Di z6?eM!+1@I}Bcp;;u8m5Ynqr{>0kvPMIp#OW<7qe-sL5wB1aH)%rMP0n_P#pdttZ5$ zmri#-^%?z7hh|Cy$R~Op*yBcGc%#a4hIvTWk9LON^v9kKsB~t&JUHx>MZPwgl1z#& zGaa|GTQ0(q_RzLYh*(Tl`i;*psDKk=Vi(>+%OFcZU1a+vd>)A!$z_8g+%1A8MT3d< zW@Y?E|8Z*ktoRu>9~iGcZX#%7^VzcZt44p;<4Etnq6x#l5PrW13<%` znl}mDUASbBbskc#;%VAhRJ`w@Z?onU$Lv!u#i`4K?G3Gk^vp@lxy*X*s;Yg|Z{b=0 z>=;owZSo;*ur==P1g$p|85<7K^2SKGmnqda<0O|IdxpW{_Npd4_F;05f}o6egJplI z-O*cU0BXS1VHMv}ivTbG(j*=imu#^q@nI0vI{Ok6Vh526PF$`(+|7HzGF_k}*(IOw zT5DFOngvWiTf@VYI0SSa7r?5%VL2!&<2I_S)dX zqEt^Iw(YXG2~e|T_o*!C5q5ap!P^!2c%GBN*Jk);AG`q!OYepw->wDl2FnPbB}Lfq zW3&sHLU5y^&Zei`d`Lrza*m>?8~|qC7Xa-`D65jnfWeBNkyR^A++x}!5rANMzmTUt z0T*&I;`1FjXLIrn@|SbZZP)Y9?{GRYU>mpAH%-XRhK;ekRhl^x$Ht)THIqe(HO2^E zLlD)g0NLA~PGJ?HJU66%&oVpps*20=Fe2RZr2b^0{MKfD{`J`HUA#M5(KIef=3sO2 z`Ku!T+FU;Fq*oQ6sf9ZdA3>1!`4ndK2hwbh zEJEh!vW1UOcdh##8Fwg(fp_(>z(;_EXT2bYp2TWkBoC0dGeW^oGxK)Y=9? z4TGh)In9P1Pq)IVd3BbvE7EWB@0|y;?X=L`FV8+k zKTQ70qV(ea^p#NLqrrEGsD=c2PY;M%zSh`SBWW6VdakpA@b@j9%$r!zURSzx=~w=# zoT_w=fxqEyBeW_U5rBKvK~V!f;F4A!p=V!is9=Rhd{oGJ`g=slN(hGeuuWyV42N&3 z=A+Y(wzBOgO1j{C$j0B%_l2wqQ3c{yY@`tp^#&N;!un7A}Rv$5UX^;FlwTL?Yw0Qd_G4t0#3H)xS;bs zTgeeKnuBlA%L^Ldc_HSWL0m8HAFI3uwoTSESA&>n z!P}gpR-f+w$j-g>uqo5hrQw=Nkbyz%oMC1VPy!h}k?{y4s3pSO;1siG*XA{)nT_^_ zW|Gp=1`{IhqqUa4t{&wO5Z#0}Y&j3-k!*%CEhaW*uu5-5BoO|R5*R>fie|3nbc!F# zin{ATmRR%v*OPu!b;}gFyRn!#OJ^;L$|KLlr2iPt()24s^}OHC+a~nF=N(iQEg&H6RjZBi+15nUZY%Pm1$uaK7-|=hQ`+P$fC$#0{<2e`>Bt+LZei0SAfnDl!jWRrWU?SI4xp{y^p zgx2L7v#)Ca1w=laCu4QB5UK7H4S~BpYI7eeF<=TxnESz6H>UXg9SEtD#QR?16Kq31*f{G;UFHC&<=7KX{n&hqo(~74|F|GfT z{1X*H2a&Uakaa4p65?Igve~Mtu(HvxaDLwl;rZw=B`Ceu4&;2iMRqFKd`^9c6r_dT{v)KTKx|5+UxLNz2U6&3x=mo-P@=IM$_BE#Jedku!z zPiC%;DmnFAZruOhX_KYNK^mOj9VHLu!naJ!$<1wEKjhKta{DibK}^LrY_L3QCp|!= zsKZ{fLU%grd(+~9mWBpXujqB{q)jU&Lg??;9smB{*fF_C{qG?Secq~GrgB5+A1&5T zm-<3=|$+tvV8 z{t@@NOwMx!{?&3bu=jtaz{G>@EKET_#=hUh&ou@{>d97Asi7zTKp@MgC@h# z>MxJ9U4Yhq(>_wa2FxY z<#K4{Pl5fz_Cek1bZ?PhJ!^pLcLnh4zC2iYTII4wlXi;dCwucjf(ld~2hYxT5)*~p z&{5!>91W8_%m+ZgKHgLAIjm{i{_Q=^BB<2I!kj{jO~zGREj~gL%{i(70RX_W)%*LPK9t z`62ul`G<&YZQhN3)Sk@eo0WQDP-T4Df8}{MQ676n)1F1K(D)^EBPuUy&R{ zUIEs`Hjt?pm0vL~YoGUa8uxmmw0FEsCoWv(pBcrWmf_S}&(z0}L~A4nI^ospRanG? zM@21r6y3>t(-xQofrIXnP5@GG3Hpf|)z7ViFaHI2X# zEC9*A6=WBvP|;`1ZB<%d`Jbo27=K)Pj=@G!DNt9 zlIJ*e7CtY2%wBh(ygFS6`wV;g`zcBZB+yoG&@fYP3nsGzVB@CSEB!Xupr58_o_G)b zKg(x>^NEaG7T^*5iV5 zI8d3^{QtQA9}LL@YC+_@J&rI9HMMrY08s-4`4ycMm5V{3J78GUA2emEi>#3-ZN6>T zZ6PQCly+%(Ps;l<*rPbkBXgA{HW z&8POq&HJ37*}F5DhCx+zcD@cgSmjg7cr}jN#`@Qj_>7{aU_-?-U-id@AlhUEal=F~ zbTf&nR#F0APq3Q*b6-SxP*votwFkH=O^NjXdaR^#5P@b(jY(!`xnVu;N+2XhAu^}; z!QHh@&+#}&Zcm@T6U?=+%p&Zi9LYcnYdABJy+ zD$0Ev!}@E{Pz4-BkQm-c3sWcw%LXXyi?gFmbJk1XqXSy!L9>$!LnZ;U(7y}L5N~8| zX0~v7JX6`rxCvZyk-f0%LD84_co7fRH+MMx%nq1+6L}CGRGE~AB74@>)<|4d7jn+- zVEwl&;5;7_)RX`UIXOAQB#)!w(qk{+AbIpGi4y?AwsI>JL^h)C`se?mAd=5Y5}MbH z$8kHY9|axgCn8QDwV(U(v43=w89^!Z`nH0q?au%Zcdy6s3}mYa)+{w@2R3TBHCdgK zp8~-1qU)3~g7yE2d&!Vz7|G&(coFM`4Z3&&D>ML{hy0Hq_9OP-knGsOb%61Z?C#+j zVAm;WmX=~A?7Gi3Q*&NfTIK^XY^LWr=8!wZ`;P~LvST{e_5TczOJ7L6ndW>Y#Nu(z zw)$rGJ)MyYUAuY3h|4%&30H)+)<+7hLlP8nt6w&~W$e_**D{=0-vhj2al0`ffm_~o zSDKb6->^=9ybY(HaiK6kpxP~Ug|t$8y#N89Lld+r z#lQK6Rbsm+i^woGfa^u@xH8Y4h1{SPdEq$iVtfjyBa1h#CqMlf=>`s zFmRQuYHDhGYq^E!$6s*+Tzh*btQ**zrd={OFNbFx7TQAgfODC6u7uJDXe!<+c_9wO zBz`JiREg93em8+B{I4gb!LRc!oZ(RxI+nAEb*MhCm=rjBN|#rCIIP_X9REhE8_)XzKkv`?ji9!2DWTP87GBtHA9s-+7w-kk5MNxum_9)YF<{AXx zPy{Jd3n($53yiGKK(?d^R@bC2L3bQfz;2(6IuaANnom9Q z+y>2yO`u%{hyBw}M66^=a(}&ydZL2TcFG4NWOzzUA|+y7o4C z3~KkH6;XMCcglV$Yf~G<#KcEUkGEXM+iu4`hOOy2|3ni^2ov$o0)2O1bDIwZz*8u% z#{dn=7-4gK1d;@~FI2*ll^1_C-}r5|KA`iI7fU3S#i})kGK|oDtsY@$#2nQNg#3v9 zAB~2yhy)y2ZAk~AMvzMLBKrS)oitWwhkZ~m))@ay1<QZ3ns@PN)7XY`>Pbn zXH^RSTQ^x^NjTgJHL~q`*y?{Cd~b+ZU7S8PHt-k;c@#0&vJ=ze%k%ZuJDZRp{h6akmjX}4{*%p}hmgd3@l zRHb(G0!&Futv=7c7hrzs15S6C>&q>y*Rr4P1uaQZ2X|7>9=Lhj|DWXp!2&JC3K{f1 zIvFCibrS~l=<)|(bBUa0gUq01PW#99u@p>n_+O7T^BY9{*dE4&*uM*BH^rswaOqS=Ru6q&i=h{A2yi!Y2YdlJ-MR=PQ3d-rJxQpF7!ZU@`iNxPYH}L zS#`7&Dinkl4D2~RMP#2g$!E`dngdBEtWNBDg)4^=cK)~IC;$twf=DGmR+n^Nr)Dtd zR0u@p#eO-C@-NHoYuT9wAXL5CU-&EV2dhAUma_GBgkQAvGr95d^~OYHVK<18)I?ue z=@A6f4@#h%`ZxNokY{MgfT7$2TfI|FL+37~pyLgznN+Ep{Gf->;OtC9n`IZUFj%=lFu2#`8S`9qNW1;Viabt5$Z!-2Hbx z<~ss^0i;dPuiO>OEoamd&jQjk;@`E~B`KpG$-iue22tiAsa6h0_98kKyW>-iXfP6kHt5TQSD2pKg6bUS>G z4Iutk`4i z@caDuL@3CpbGJ+=9?C+NXzzTL(H5)oMfYQ$<3_9ylmx(4Mtx1m%m8JBc^@06rrRv^ zZhW{3zy`3M%qGwZdv)f&?)}8}Yd-qv8g+I|Ul}oB(ge%?QWMV?-0ITVA^*{!>4as! z8QU=x!u#vKZ^7`eASL|ZCH%Y_j5$IQ$DWbMA{4$f@EQQ<=b!Je{5271C;$z^Zpb?R zvw?gZIND6j&7|KACGZ;W8IHvFI1*hOIDq?=3<%KDOHw3&LV-S3es~|Mv#9lS> z@ebzA4}G#!fSAs!>YE55PtX0a8$NGAUk;o9_osIW+JupN3HRNPr7}MPiK$?@a<_F) z2-FX+T7y6~egtwglfXn}ojS_&CMI@GrMhFgLCUP>G~eIvnU#yjPWFq(kPAnKAFVB5 z3b2tA6tee@KKD8~nRSN+4HraokY6jtudS(W1PKxN%R#k84~q+}cCuqu=m~_z$!c63 zUbvJHK;F{<%#~Rx)|D<;9Fjh8J+QF)x27q0%;sgot|^t?d1yE@#Ef4^g3^ zwS^+Y5;O#0IVBE8Nsv*M>1LystNbeNjV0{3ABR5=GrfQWVR0tZ9YsKQGhV8|X6sNY z!=RZ8yT)CSm_*(HJjI!%QISNom2VV$okrxf8?)JC-&v5KTu&=L1;Q2(lL`=}84L^3 zWpL~EK6|J*xp#5#GCQ|0tlw!m5*i`}bgK~`WWok?c^v(r1qjjeI6-PHxV1N+YWj;h zQCZy9<##nyV_|_F%1*Pr&6N%fkSt-ft|!RG&9;KLX28+ zT~a(5j^$&4P$Ul1i&x_V|NH?3FSI40C5BvJ_+x`MFP8n5@+_UIXc#1-a_W9i??@Wv zm6s56q9q2GW(zqgzP|#%$px5(fmZwmCMwT5yU6~RchD9zAtT#UBjl_V&H*-#%^0ft z^R0f6g?J=h+QmuQ)W%;{wn~CoR%lV(nS)D5DOmBMhY*w}faNQbr&IqGieV!YpHiB8?;D zsDFGXCIf<_Tun3hTMW~{W061v7jXaZCn(bd+XLH33jdTe@cwmqIkN51tS^yRVd=&VRLNqawJG zi~T*-i&Mwl`#N+zinr)Vh4<4|R0L%yr8(7W(h9jF^&mU zJgS02d6`?;=4^kdkv!F43n55s$u4gmOhw9P=GUC?R=leyCrhFa-h@!yavv5w z(b&z?y_>jpkL7d)W&f^)Rt0I?-l7lmJh!l@PChX<_H$Eawo0?!7V*B)iBJ}$8kEKZ zOvh@e!dPNF?s~e*&K)u{^sd`*=(FO%cn_C2i-slL1xv?rby9U@%3XMI z>Ukt+8N^300YuW84FRW5bUjvyl`mE)Y|L_+N1Rx9kGB#In=`zDW@T3JC&TX3Iw&OF zFhjDVOg08uc6R^Kj~}Rrv9rKm)=#dVs-I26=2lKC2As2}^b1wDLTd@yU!;8nI-Q=6%O?Zhb^CEl-D)e1`zie#<1<-Fu~>KnE6!$7Hup zFfSVBC#QghV7DIl#Rkv?>p_BC#-l)gQpQ6w6<&AGq2O^u^gd+;A??)7BKz-F36213 ztm*Qcndr}U3T^;L{{K0LpVAx1sxmpft}bT#vPW9K#vJT*a>#)S>Wxbp>DF12*Hty5 zIu22nTeD5zTmt#5{#tZePUxTEErdc9Fp>*v_W}?<6*GK}&`AO~V{D|RHXrFA^@~Vo zoK;PGfha)cT|ffg|Npi3Rbf?jUAqz@2nYfqAtBuj3nW!QdI^G*fC!5Y=@t-?mTpPu z?pTx{-HkNT-JN?b{N8u}2YVm>XWw;k#I;xt&wR!l&m8x-?|WEJ*$&PF#XD!go#E2C zSBz(KZj9&aU1brG36voUowkw0#Z|K$Gw%N+c%2KZmShNby(#KBR&JT+t2T#hEiTlW&*%s}Pl8~eo z7b`L`GkZmjMs@Er+-Rc#6#o3I<*^RS)?_8lr!qqk8M+cZ4c(Eve(F+R03W!ut#|nZhhK~uO7vs{vjzaWrJ|TX53vygDpQg-ls-r zA}O6+E*iY3@T{I#bzj$MS}>Lyeka>n&hrs_iwP77pCfu*Uv{%63OBgbT}TCqXoPOx*u`QUqo0rxa`!8MeQQEV*pH)2i#@&d%szy6*AyYl!#4O z`TGNA^>Uo)BA_0jqZKzka(6Bs72=bxY2>YPMYzWd0y`i7iZ3R(+0%=F6djHIAz1iW z4Y=c&>jR9+8aTR4&Yg5J7e)XZm@dF!qBTr(%GhkUx!woBcL!kXF#(!=b$&2fHmHY^ zuybZz59DjiMGvaPdPLvd*V;yF*mjnfb!CMW_I;e4NRptt!^Ro>)NBeiM%U%!1zza{ zJxTO1Zf#>FGa%+u`hYyxs^wr>@*RSn_A+_?ShGdFLHmhY_cAp5vavztXeYzpnAPgG z{K3O*H@cIvGH2KNoRZ&UIExQ5 zuyR6fH>_CEb&VhmH<}n5XHS7ju43tpmjap2vrm_f@nZ&{ z{x;@FP=i4msT*4A$mnQ#YSQ>uT>CPkd&39SJ2*a%Wu!ueqgF0$#ji``UChKQf zy1hI8)EQ-PoU5kT?uftze2qkXvfO?le7EU*%}VjoqREZ3P@_RpD^14OuLH{}<+^n- z1$lH5Ltj0&kL;ApNND0R^^+hy2hhd3W3-AuHcM|RTK$Ox?r!}*eePu=RrxQ`u)v$V9V zy_S|v65+F6{pPaW=NiFB8IerteV?JD#iQ0axz|Jcv~j~w7pWI6((@f}P#CVJM^YsH z7Smq_@UYjRHOC)1=e#ViMOR+^nDg<>TO0G28~Ij=+9wC0t=WuMPOLgLI_}pP!gDDXqXBxW6y^O~>n$fgQE;%n?F|2A_a1?)+)$ohG(iRE zy_FN2M~kET8x(U?_)_Z)%UYZ89gwG=mGRrN7=LWc8c;gx?E1ikXHGmgH^kW}H{vP! z7=Hj&74zi9fdbE}abls1qU_sxle9M8;o38iA8v(h3hXrJZJYx(5(k5#=p9*^+N+08 zq<9MTpFmIESu%U{_q}vKNfk!>Gdko-^+}ibj62))EIn%F znX{KTXf<<3aT}yn3Ba*wa4{|(VVm&%);3{cKYK-i3rla$=7eG!c$)boipE(2%Y>zm zT`%sCY#XK9tB=m!Eu{CJsa?26u6BS!3$zFNou7EgCYjR$Eif@InP*Y{a_dS1ZUE2o})Qb7rg3B%~m$<(HrNfGqz3*b>Q_G)Pw7^j!s8EICXtk2GXcilR8N65!tC5u`ZB~YpitG) zrf2Kv(S<_P1inJAxZ-bmWGCj3J;MG9{UZYkvaxK|FXvlYHNRfgv`uf7GwWok`17Fy z0MmTy{7t4p&|0r88MkxGBh~gY;~_Ijr^V1${@=0(_otm!5^iCmQ5@D%`(GOnKqaml zwJ+Z2Q6?!{hg8OdpJlvt7%u!U1FC~|3?!RRV^HG>%nthdQtA0>9dOn;y8O0T&qi6~3MMZ)Nor;wC5JLP#m!?u`%*^rxMz zRS|A!+>f>uQcLTLD=Zumprtk@F3;hPH=WcK#cG;PIN+A%M4O9Q575_ROjkbJ#*OMs$omRA>VIgwi5UMHY+PN$ z8p*Vlzv@bMM>(=Am{|zD-Fb?#g|r%k7UD9CM$MJxi$-kqmr50pi45lH{bEWtRrTlV zxw?=60m-&NVm*|Re)Fb+C%P#YuY?Zuuw6AVIW#X=cqnewgtz6;8A}K^l2P8h=U$Cc zL%T)+SpNe9=0FesqkqE%Tqo>_;C*W6wO}g_WCy7`27EdZr4OH2}{GTo7GM^Q2<}N_w0~;~64- zcBJ0o$!1Vub8z_J7Eu=Szbn&&za__Pbg&rX6EDTNAQoS@objl68liJEkx-}iRMy=Y=Bd{fGcX< zk2aaevz3f_fh4Ij>YK8K5L@*%K})}z%E!KDGrzRFlh79<#X4l!rPjJ3W|kh`e&q^m zi|=92h!n0%!AewTUQEBex za)5jdJ$Ym*0Jmgh(w7(C^w>IRdBF}h-IMcX$IG0OpYB>(bqo;)l7M212Ld-XpNVS>bh}mAjns< zNUBQn+D37&7tG9Oa7{$#sX-YZ?bNeq)uY-K68ITgDA`eCIr)d|8-_UV+7a*j0#De` z4Kt8G7Y2a70d6L!zpa{J(+11IR@0%c#y^Y;&L0&x4sB^4UWn2RU(HIQe@{H%CcY^YasR0=< zcNP19Y}-h`9Brq00hxVAay%)@xKxra`;=9#Q4W}fMf=2}uva|*CBB!(tZQQqd^_0q zH+~I+I1;ERq^~?hqwV{IsXy6N*60;D&1|8`0$nj?sMj;<5M=~%W4)|yu$(~$To;rs z$1^KThw>8)S0Bs|(Q{r6zgTBmL399sto>PjSj87YRxPUQ2V}qpb=3UrfO@%%|B4$c z5;sMb!>MnO&hN=7|3|oh62@bT=fsE!)cr~6SxcU$8W>v?q zj)Poh{f4qg-C-}zeIPBha9O50_Cfc|rLl%wLR&`1SLT9;7zd||M*DhX2QPcRoz91RT(uAt!M_@t>(*aR-h7 ziXQPH{P=&7TB*+-hI=nCVv18!IxtStS0~R>0vvh6L#lh;QL|NH|G)r(k|3XU+???%W~%ShPwXis2U=N3 zu!!9)3NDr7@$HDkZS5}O^(8k(3~BIsPwBkfHw}=ccmVKdzTPPKx7mq@XY|`JY!%^w zB2EaX$0gWF2zcNxpq3^E?*49;{=be*l(^~uvuWpZ{d@-*-0cBFRIl?Z;BexKg60qG zGGbYn1In!Cba)#+ljuE$74l z>v%|Oct5j`{*l9#v{0^U?5lp?o*(O%0W|%>@HU2u?vTH|m26z#rtz1yiHEnFr#($` zI^2z%NCn!;GbZ(q9A=&-rpq>G^ET#Uya%#!muM+Bt=!4ITToHV8Kkp{{5@J&ot_GzaxYf12n59B*AaLexPlcAu9zpyHxa zzjin!u-e*&%5q9#pHKCGnn65mezcM0%MJa=mekq1!?htcq56| z1>{VND-ZoIJjaPZ^L=d7c8w+%Hh191P`8+|hm#gciEhCUN>Ai)mq3}+V*X1=@^icP9v zwgn)`+vovJmE-$>F<;mAH4G#UtKwm@BiW5Pa4>_+=#Wz@z-UEo?@fS74AbZvIlq69 zSPJ?oen<}6Xm^QU!D0$_#|H z%hx(VxgA@!tPL|lI-!iY5L3h_%h_x1vXH-?-m~)mX;K#5uk&d)PBzsbh}Fatlxx(K@9>tiBaZ*%Ns8l82*h@*4Ii@L3*tdO#E6kGC!v_rNM6e4LTRW)BouT z-llat&|j0k6DDqEUI7~Xwl-G$qhr} zmlqL1OI;ayNkI*ne&2(uD_B9d(Y~31{XeHQ%8&)*GLPj`q;B!O6F`R_2T%=n@dn?C zx{?xAIGudhOr1+*wZ$`4j$ednB_&)5HGx%ypvw@j!2eLR&l|`2c+)u81{k-$C9HJ6 zxh?_z#QG}Z6P1>hJ%DQ*wP&rOto*wKNXvv5VA5%K1h>k85P}Ba5nhwWGy~v2 zcISlNuliq2rM0xQ_z2P|yXIn+cQ@B_S=xVzrT>#8J)6K>h5C=*czGrM9(;j#2hj}7 zhHf_lX~sJ~v)}z+WP__U;?y6Di{i*{ zpAo}ZR0@F|yrz29$COpv;F6M(ISa168q#c**h^vT&zJ5lE99nahb?;}Z@-9^(LNv8 z$N$`_>4naOr=&!XZ^%4D(kGs#{alD7NE{=QX>a=%Gn4k7iFEo1gFC^VRPJ8z*RYHr zRE)4<(Jo8SPw4Fy;I`Qj5MFV0u-=K~4t5)LJ) z<~smi43jkj8W0ES<)vty3gFP4oL=muJ3)jXv77Ge%ITWbU7v4AN^pY)8!5!%-PS2U z{WLy!6sQ$9kvlCu#2j+?eR+1Uh0sKB_X!OxIU-aZfXimRKqIFDD?;=+Ot~H$N+z;E zh4#Lf|1N5j{Ptu`)CD+Nn}?zqX$$DxieSj00zi>d1DN5PfG_a}}&h1I%S`O1A*MQp=wP6v(vNb$Z zRU3EKor`H)RjyGu#03c})Pe(ASi>aX4jTb~_%d(Cf!a^Y>NMqX>WM5w?OTIGw(9FH zpYL!c%TV3%-2EJk5a0Oh0oBo~6y$8r>Un?W)JSI4^OK%<$c5qVJBdch!>xwvbKsEH zp#Oow2Im?yHan)%D_-7xBA@V#SEJswTBiXBBp8%k?G8xH&SLtw6B|DE#PJRQ7m6)w zggKHT$G23DBR_X{&@1|ZXsK1kNBV`273xsj#W6u`kYto4ED!`+S~$s;91(W}LQD^B zGSGI^w>TOwSM-^vWcT4ElWW!n*TX=_;bJd(b@;{>2%f4oi!ii17>M{4cf-ciu7Ht{ z;%?0Gh12m~?Z;V_H@o(dDaDE|+Y~OF#jBuqbq1}FAc_vP9LkU%_=16NvgG|FSmZpb zD>S`EBIeP+w2*qqp>2`=8T5I?RA?UNWXf1jvqEQEwK{yptnN)(ZFtFDLoosp-*T z2^gg=ClGChT4NAlW$!(lFi$~pIZ~V{dy(Y4&L~P4Nd1f2E83t>_@s;j$;!AHF>-{= zan5sQkS=`pLn+8*lfZamVS3jwZFeFJO;cpscqB(fQmqL={YB)f8#b+wkHpUoXZuZJ z>HwozzKaJ)$WD&`tdRka_q$$Qb-iGQ!M@P?Racm|^-t=Df$kT8AsjeRv`h7e6(ce` zTG8=&b#(k0vOv=I$4UwT((zJ57d3QU zVj(@i%I8e{waaC54tmTqQUBRx}|W%vg?kpyHP4f zWmzmvOPP9ZsQMnq#j9(qSbe3v&vN-xa9eRCUw{AIO5}?l*#+8;TflgV7yfRwGcy;G zSEOgFI0%!jTm;W&3+=g)C&?q28BOaM&w+I1#e7SEIcTeu4{^Hsu~VUOmsL!+qbFu* zbT7T{bQp(4kAuGLdMkS?&Lbv_g&_Vc{zfcnVTB<5CaL?{1}+Oh5VLsmiaqE&#Q|17 zi#pJ3tf1j*LBSg)pOoOSj%%^}cm9Ev6c&zdM_t?(V*DscfUcR!3Yes%SQ0S5ozhK1 z_;=1PF>odU7=bHigr5UeBGl!N_v0y!x^4Kz6cJ$7u1pbmW1=x&djR_D;5X50M|W*U zr)YWED3-l_;dx(b6ZPE>)Vg2xCwFUZt}onls9S|_U`vxGx%r!|q?To%={z3e?|*98 z861&W)5C$uO#5i?c6$&A+KUDmm9KA`HKZYQog0U{WBq+63jxFwRF{myzbu&E`48?9u@K-T1nAq z+jVU2L+$%>(?m&&cY+J|zM2BU*73S1qNBDaHgd*0^k;Z3fziCN?>kF_$1aB7K+C1W z=|)IQf{4PQ>SvBAUty?qZJ%A}XJYufL7)ns2$+3USDfw%ridczr^!v;pIcp5U<3G9c3L8Dj@V}-`tU`J(vEb2>{ zS63L9ld8Ynlj@I%khCwcvZNKVx@Dh0-+ceQEb%xqjWh0v8!5%g(r3sj=vN)xHvwJr z8D1g^_I={fD|hjx=X_lvs%rOc0Grz&Z%x%(jmselkBS zMb{e-rc2<^rQo%dKJgK6cA^nI&~GgDdq_xsy1VZmjV;&NBydD$%T6VGDX1bcwA4L^ z1D^tJoOv&L7t@mW`3flDhXTM&>9idlp9r$Q^)hF16nk6-S zQ1WlO@BTEl_+ofjg2{Q`>J@a7=rO0q0|yPnd1hW1;PZpYG$yS|&k7GuQcKbb+0X01 zxnXW0w&g%7wR`V(tM;_Prv-3!J5tBk<(W@N3H=PoFrqRDRe2M}TvgP=tq{$NAxyQ1 z9IRTJo#$$McHk1h?x(isc<7dE2Z2V}uNjwK4y}yjVru4(8OMgrg7s1v>l*cLZ&^KY zWD-5}kI_%`vF89ssvN{h$wfI-T7sV6q->Sn#jk*0$|N1Oz#ow4%_Z)nmk)iQ#bMRY zMFFQtM`gcQYV<@xhQv6kR<-XCG@|^qs$Ah2DC7tniwv822bZ8-@y|tjGMM)NjHGWw zve5?$qM6O9&#`wCzHg@BqF|JSXdU&?73j`A-b-&5;X1m{S>^1sXIUcpZt9}r``rrDd~BEw45PsDfB{Y@y(^I1)sJ9d{u z=@=Y?t8PL?Ib$XE*_mg7Vnpcs=T@?F@5hO1s!CDY87s{4GqTn-Sr&_kR}7Ys8Qr7E z^jM*T-fY#!se&M;Scq@eD3^n7^UE&IVZ_XRENGKY|H$pR^LCNCnJz4*3e)*(ik0u} zCi?EAt!e4zAgQ}BAp4bvhg#P4 zL2c8p&=nd_OIry@PAOevFJq&PV!^yWu7;si5;*Y!|AOSosNe%=C-pX4p@TxCn`6rR z@0*=$VPJbOP}C-`p^-F9n}9@;p5UyJ_GG`;+(&z*BV6RI%2@8O_q#4Zs;fk!d!;Pu zb1D+h$_O}z)^@_B7Mc-#bfW%aq=&eFe>@-1Bop;NVBzs&L8hYo&rgD!*#F~mlqh8= znDSHs9_YS*f5hnVLy#sq(&@(~#8Acm{v@MdwBOnK`69d>d|Uq0pR0%b#hdg8*-U=7 zf1^wfzUA4=C2*yFZ&bBdFmX&FXL?+gy-t7g4c_g)l%xv8|Tt_c@558MF?GVv?i=2`YtBnX-~=J zGO#i<1JzIc#8?F5o4|Bcbvys7yiP8cV>2CX-i((H*FfX8tUZFsR`oNAO^bwLFcWci zhfk1*lZ90m;kw`glBjAE)`zPD^-Z|4?O<5j=rPc}jPEaXT@99-j=?=UtiAHCdYy-W zdfk4(8dTeSx=pAqKsn-8t;Y5<{2A~9)SKP{Z-p~|PK0mT*})0~PofR6X35MLS(x?A z0V)t$WUANRKY`wItDQQy3=CQ86;3|dpQy6>L7I-3Kvujs2Pz{27J+L04m{C>^o!-* zgbhiC)>RPdMnGQ~%U;|G-@4c=4ekN*Gce5TtuscSv$b&&;&yD2!R;ixmI+ft6@MQ< z{S!Wh&`5Q8-?IfSSyyPRTVR%&g{^f{j!Lc>C?{uxDQ4Txk2cJ~1UE^D=-BPE6Jvh} z9=2?HzRM$|heRC62$?021CH#If=O-I6e6{=C#PNDy0wDttNry^^ntya7S%0b7xM>y zpV_nmh^flxqHQR+Z2}~$PEQ$75>z^j^?es8HTYPdS=swGPfgcRCK3@z$z$k;ylpO9 zy><7QtQvg8m+qtTS-|Bz5Fp}3 zWeTqT&%Q!>Xe5ec*I3zrMdSI;e)<1?jyQRX9Yy+|-3M-1SOP2-^iie7+kf_z80rKR z{O+Pl4DUa?4{Q!GDB_vCX~~ZV&*YyCeT>+Y*IXf}|88fh{x^tYu(H*L!T;E(SE$!W ZFD3&hvg<^d1ux!z{y&Dax~TvF literal 0 HcmV?d00001 diff --git a/man/int_times_int.Rd b/man/int_times_int.Rd deleted file mode 100644 index 565baee..0000000 --- a/man/int_times_int.Rd +++ /dev/null @@ -1,19 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/000-wrappers.R -\name{int_times_int} -\alias{int_times_int} -\title{Multiply Input By Another Input} -\usage{ -int_times_int(x, y) -} -\arguments{ -\item{x}{An integer vector.} - -\item{y}{An integer to multiply.} -} -\value{ -An integer vector with values multiplied by \code{y}. -} -\description{ -Multiply Input By Another Input -} diff --git a/man/sum_with_threads.Rd b/man/sum_with_threads.Rd new file mode 100644 index 0000000..06fbfbc --- /dev/null +++ b/man/sum_with_threads.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/000-wrappers.R +\name{sum_with_threads} +\alias{sum_with_threads} +\title{Calculate the sum of a vector of integers using multiple threads.} +\usage{ +sum_with_threads(x, n) +} +\arguments{ +\item{x}{A vector of integers to sum over.} + +\item{n}{The number of threads used to compute this calculation (int).} +} +\value{ +The sum of all elements of the input vector. +} +\description{ +Calculate the sum of a vector of integers using multiple threads. +} diff --git a/man/to_upper.Rd b/man/to_upper.Rd deleted file mode 100644 index e2d9321..0000000 --- a/man/to_upper.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/000-wrappers.R -\name{to_upper} -\alias{to_upper} -\title{Convert Input To Upper-Case} -\usage{ -to_upper(x) -} -\arguments{ -\item{x}{A character vector.} -} -\value{ -A character vector with upper case version of the input. -} -\description{ -Convert Input To Upper-Case -} diff --git a/src/init.c b/src/init.c index ef313b6..2e0293f 100644 --- a/src/init.c +++ b/src/init.c @@ -34,44 +34,14 @@ SEXP handle_result(SEXP res_) { return (SEXP)res; } -SEXP savvy_to_upper__impl(SEXP c_arg__x) { - SEXP res = savvy_to_upper__ffi(c_arg__x); - return handle_result(res); -} - -SEXP savvy_int_times_int__impl(SEXP c_arg__x, SEXP c_arg__y) { - SEXP res = savvy_int_times_int__ffi(c_arg__x, c_arg__y); - return handle_result(res); -} - -SEXP savvy_Person_new__impl(void) { - SEXP res = savvy_Person_new__ffi(); - return handle_result(res); -} - -SEXP savvy_Person_set_name__impl(SEXP self__, SEXP c_arg__name) { - SEXP res = savvy_Person_set_name__ffi(self__, c_arg__name); - return handle_result(res); -} - -SEXP savvy_Person_name__impl(SEXP self__) { - SEXP res = savvy_Person_name__ffi(self__); - return handle_result(res); -} - -SEXP savvy_Person_associated_function__impl(void) { - SEXP res = savvy_Person_associated_function__ffi(); +SEXP savvy_sum_with_threads__impl(SEXP c_arg__x, SEXP c_arg__n) { + SEXP res = savvy_sum_with_threads__ffi(c_arg__x, c_arg__n); return handle_result(res); } static const R_CallMethodDef CallEntries[] = { - {"savvy_to_upper__impl", (DL_FUNC) &savvy_to_upper__impl, 1}, - {"savvy_int_times_int__impl", (DL_FUNC) &savvy_int_times_int__impl, 2}, - {"savvy_Person_new__impl", (DL_FUNC) &savvy_Person_new__impl, 0}, - {"savvy_Person_set_name__impl", (DL_FUNC) &savvy_Person_set_name__impl, 2}, - {"savvy_Person_name__impl", (DL_FUNC) &savvy_Person_name__impl, 1}, - {"savvy_Person_associated_function__impl", (DL_FUNC) &savvy_Person_associated_function__impl, 0}, + {"savvy_sum_with_threads__impl", (DL_FUNC) &savvy_sum_with_threads__impl, 2}, {NULL, NULL, 0} }; diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 96fe1ab..d08356c 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -1,6 +1,13 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 + +[[package]] +name = "blazr" +version = "0.1.0" +dependencies = [ + "savvy", +] [[package]] name = "cc" @@ -11,13 +18,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "blazr" -version = "0.1.0" -dependencies = [ - "savvy", -] - [[package]] name = "once_cell" version = "1.20.2" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 2986339..2b6984b 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -9,6 +9,9 @@ crate-type = ["staticlib", "lib"] [dependencies] savvy = "*" +[features] +savvy-test = [] + [profile.release] # By default, on release build, savvy terminates the R session when a panic # occurs. This is the right behavior in that a panic means such a fatal event diff --git a/src/rust/api.h b/src/rust/api.h index c5f3fd1..e3ea717 100644 --- a/src/rust/api.h +++ b/src/rust/api.h @@ -1,8 +1 @@ -SEXP savvy_to_upper__ffi(SEXP c_arg__x); -SEXP savvy_int_times_int__ffi(SEXP c_arg__x, SEXP c_arg__y); - -// methods and associated functions for Person -SEXP savvy_Person_new__ffi(void); -SEXP savvy_Person_set_name__ffi(SEXP self__, SEXP c_arg__name); -SEXP savvy_Person_name__ffi(SEXP self__); -SEXP savvy_Person_associated_function__ffi(void); \ No newline at end of file +SEXP savvy_sum_with_threads__ffi(SEXP c_arg__x, SEXP c_arg__n); diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index 9b94e3e..280897a 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -1,117 +1,106 @@ -// Example functions +use savvy::{savvy, IntegerSexp, Sexp}; +use std::thread; -use savvy::savvy; - -use savvy::{IntegerSexp, OwnedIntegerSexp, OwnedStringSexp, StringSexp}; - -use savvy::NotAvailableValue; - -/// Convert Input To Upper-Case +/// Calculate the sum of a vector of integers using multiple threads. +/// +/// @param x A vector of integers to sum over. +/// @param n The number of threads used to compute this calculation (int). +/// +/// @return The sum of all elements of the input vector. /// -/// @param x A character vector. -/// @returns A character vector with upper case version of the input. /// @export #[savvy] -fn to_upper(x: StringSexp) -> savvy::Result { - let mut out = OwnedStringSexp::new(x.len())?; +fn sum_with_threads(x: IntegerSexp, n: i32) -> savvy::Result { + let x_rust = x.to_vec(); + let n_usize: usize = n as usize; - for (i, e) in x.iter().enumerate() { - if e.is_na() { - out.set_na(i)?; - continue; - } + let out = sum_with_threads_impl(x_rust, n_usize); + out.try_into() +} - let e_upper = e.to_uppercase(); - out.set_elt(i, &e_upper)?; +fn sum_with_threads_impl(x: Vec, n: usize) -> i32 { + if x.is_empty() { + eprintln!("Input vector is empty. Returning 0."); + return 0; } - Ok(out.into()) -} + let n = n.min(x.len()); + let chunk_size = (x.len() + n - 1) / n; -/// Multiply Input By Another Input -/// -/// @param x An integer vector. -/// @param y An integer to multiply. -/// @returns An integer vector with values multiplied by `y`. -/// @export -#[savvy] -fn int_times_int(x: IntegerSexp, y: i32) -> savvy::Result { - let mut out = OwnedIntegerSexp::new(x.len())?; - - for (i, e) in x.iter().enumerate() { - if e.is_na() { - out.set_na(i)?; - } else { - out[i] = e * y; - } + let mut handles = Vec::new(); + for i in 0..n { + let chunk = x[i * chunk_size..((i + 1) * chunk_size).min(x.len())].to_vec(); + handles.push(thread::spawn(move || chunk.iter().sum::())); } - Ok(out.into()) -} + let mut total_sum = 0; + for handle in handles { + total_sum += handle.join().expect("Thread panicked"); + } -#[savvy] -struct Person { - pub name: String, + total_sum } -/// A person with a name -/// -/// @export -#[savvy] -impl Person { - fn new() -> Self { - Self { - name: "".to_string(), - } - } +#[cfg(test)] +mod tests { + use crate::sum_with_threads_impl; - fn set_name(&mut self, name: &str) -> savvy::Result<()> { - self.name = name.to_string(); - Ok(()) + #[test] + fn test_single_thread() { + let numbers = vec![1, 2, 3, 4, 5]; + let n = 1; + assert_eq!(sum_with_threads_impl(numbers, n), 15); } - fn name(&self) -> savvy::Result { - let mut out = OwnedStringSexp::new(1)?; - out.set_elt(0, &self.name)?; - Ok(out.into()) + #[test] + fn test_multiple_threads() { + let x = vec![1, 2, 3, 4]; + let num_threads = 2; + + let result = sum_with_threads_impl(x, num_threads); + assert_eq!(result, 10); } - fn associated_function() -> savvy::Result { - let mut out = OwnedStringSexp::new(1)?; - out.set_elt(0, "associated_function")?; - Ok(out.into()) + #[test] + fn test_more_threads_than_elements() { + let numbers = vec![1, 2, 3, 4, 5]; + let n = 10; + assert_eq!(sum_with_threads_impl(numbers, n), 15); } -} -// This test is run by `cargo test`. You can put tests that don't need a real -// R session here. -#[cfg(test)] -mod test1 { #[test] - fn test_person() { - let mut p = super::Person::new(); - p.set_name("foo").expect("set_name() must succeed"); - assert_eq!(&p.name, "foo"); + fn test_empty_vector() { + let numbers: Vec = vec![]; + let n = 4; + assert_eq!(sum_with_threads_impl(numbers, n), 0); } -} -// Tests marked under `#[cfg(feature = "savvy-test")]` are run by `savvy-cli test`, which -// executes the Rust code on a real R session so that you can use R things for -// testing. -#[cfg(feature = "savvy-test")] -mod test1 { - // The return type must be `savvy::Result<()>` #[test] - fn test_to_upper() -> savvy::Result<()> { - // You can create a non-owned version of input by `.as_read_only()` - let x = savvy::OwnedStringSexp::try_from_slice(["foo", "bar"])?.as_read_only(); + fn test_large_numbers() { + let numbers = vec![1_000_000, 2_000_000, 3_000_000]; + let n = 3; + assert_eq!(sum_with_threads_impl(numbers, n), 6_000_000); + } - let result = super::to_upper(x)?; + #[test] + fn test_negative_numbers() { + let numbers = vec![-1, -2, -3, -4, -5]; + let n = 2; + assert_eq!(sum_with_threads_impl(numbers, n), -15); + } - // This function compares an SEXP with the result of R code specified in - // the second argument. - savvy::assert_eq_r_code(result, r#"c("FOO", "BAR")"#); + #[test] + fn test_mixed_numbers() { + let numbers = vec![-1, 2, -3, 4, -5, 6]; + let n = 3; + assert_eq!(sum_with_threads_impl(numbers, n), 3); + } - Ok(()) + #[test] + fn test_large_vector() { + let numbers: Vec = (1..=1_000).collect(); + let n = 4; + let expected_sum: i32 = (1..=1_000).sum(); + assert_eq!(sum_with_threads_impl(numbers, n), expected_sum); } } diff --git a/tests/testthat/test-add.R b/tests/testthat/test-add.R deleted file mode 100644 index ac8727f..0000000 --- a/tests/testthat/test-add.R +++ /dev/null @@ -1,3 +0,0 @@ -test_that("addition works", { - expect_equal(add(2, 2), 4) -}) diff --git a/tests/testthat/test-sum_with_threads.R b/tests/testthat/test-sum_with_threads.R new file mode 100644 index 0000000..922192d --- /dev/null +++ b/tests/testthat/test-sum_with_threads.R @@ -0,0 +1,6 @@ +test_that("sums as expected", { + vector <- 1:10 + + out <- blazr::sum_with_threads(1:10, 5L) + expect_equal(out, sum(vector)) +})