i{F. 節t,Ryiqd8 7fD $H@Iy}%EjUk?+bͳg3.i8ک\~];U[nl)oh)!z7$]k7O9+Uq*Wnug\l/r$A]OҮ_V[ l(/GK!{#X&vB\_W9)okyj]SS5qW] oWsAZ䞼AZO~)< 7}T>x,.M™Ym\ӸT3JǺtLi%Nʶb/>W]Ǯ0WY$נJqX|Om~ϟ^>Tx4_ހr } ~s]eVhև#[m7A|?}>4|xy~zz^ZoJzZ|B3'o_Z7^퓗?ek|!l_'gM+yjI7hnj |v6`ΣwEQ&׋G7MƯiDBijJbYz^ɫgfykPݽv0uwW'|I‘vvl6 >_}u<-|iZ_2~5,ÿq#|xEXo[}ۛij?_5>Y_im%ׯe/X؏k_^o˛@ͼ7?py7oDϟ?uQW=X[ԛoσAQ6V)7)e;I@OA' nod)?"ap,p#:(\צO|J9ѓpa_/߿]۠l2h)wt__/Q\U7Vzp_ȕu34+(`~~ N3pK~ hokMY΍io_o?DdDzۘ.nO(TUDPמh]›|HoOC~>wխԋ]SU&W W,mG {'_1}ѧݾoP[Ho_K3z|]wg~KV6rr\Ewp퇏yaDDOOo?Ձ𥂛rҊ.k-&m:'GT:T^cݓʂM!$6xcD ×ɿӭh.ۛϷ]$~k4x܌O~.-Tˏo$~>ib/h>-k{K)^R_fyK׏7A+d{qׯTۓU7]^: <]VNM5*6L"Lb"l/xZ>ۏayy[_^KDrwAR_&ɷo( ת)_~|;|l|&_~O屺]{P)/e W_3~U2ߟ?G}=CoAܔ7-) }#>{7.n军hZG~~}LJ'oߣ\% @]ؿ,_~Ŗ~y kg|`~.Fm0H?X)4_נ%rKAO^H?8^Da1?Tp9D$}W8٣Nßz6]w1z ٠kH-V_7^?~|  6kܾrqO/s;|K_Bk_9$ߟo^/O?痿OP6sd.VJz?1՚| ۷oQ%B/oW ^[?~<|(~kGȴ@e߀syޗU8)^<={ti7n eqṕolkPp:} `ZS̾)Qk@مgD3[Hsp{x د~?W6?+Nu͛W]=#R3篊7#~y~3~GN{ԯ-\Es=<7F3:"tdz ̯d~ nG~u7@ !O:_~`@?@ .cۛ@M-tu {an ؓ7K4b /!R_YnR[GDl7I1qdH!j^@%XA?͛~3[dm}F֭U<._j7OqBFH+ҡC9ᯗQg>~G80-2x $ؗϏAm@'䢉#^^o_‹\߫_-uΘD׍|0?"hxo= ȲN]/@eON'Y3(7c!9¼˷?i?ޜ?a/P{z˷oXȭa7'"V?_EWq—7+/_܄-FO=y3+r|=yI'c>o2&!{7No} B_YojTXo}wP_@O MYs$ 틯0an{s;춸r9:臘Et1cය?XqHOӃmp#jO [szyo/؏?~_> ~z ^yP+٧'+R`) .ۦc!Hq/{AY09'D\F^0)@o5W3`?[z .OMGooUH ^y N&^ݽr}@Sc&_̛e$ոiTY3[[S3@Znjo~<@tipnU*C_|,fu/o??)U Lq}|({FC?P_"xA @<0%0ݿeB>~A8)^" (V@ Dճ[ .t_6m Y`={޽u}ef!XN+<}|5"|Q>#pOB*9>`bQBc(g γ RZ3VРp {:3EaTNژ3| D.xTƉBͣ7t|?_ү2y5cඁ|3jq=VO_ӯ~_K"Oo&m" c+ï>a~&o_+8}~z&܂~?_߂z~yz6+isXG6=+/`-c ~ʯ߀_9.`o~?ƀݷkءG_!=8]W?g g~{ ~i_ruJoYegs,fKVwE^]˹i<*^]'Y|<(z_0F{hb~h`o \N~Mk#Klջn6j㵊W4wymDUhIGKJtOmdS8eõb4pTHpثfM)U$yJWk+DWئIjI}`2s+h9g_vUgp:xݬfq{ã).@^~3NF"FzFϳon߹gA>Jvm|^!5a`絿>qR*xi˼e -@}fBMA(UOq&NZN[  zzÛW5J`*P_kQeOx_(lټNk8JdzXf8UOI1G3!/.fn`ۓt aǚxA'`ZbT6Ev'Yn"Jt8`;I|K83;'oֲd]64/ ! 'AY\ۦ*ڦ߁{/w7~v7?Î<.~{Wo?/ծ-VRrdC>j<2K~hDxʋ8 'ݭ``k9MW綷cݱ>8NW*MU\&]Iq<+vU~&;.<&~cZEyE(. K1LsHºd#jMRͧǻү*A۠Ks(}pk@1\P*~G".LKpHS71[m"Y*J0{9h5 mc@ ٟ8bHgcĹvJZz;q N!B%C \h+Bz~>{HTw)Ɋ-ð[`&^h2'otlwd :pd^'oj6VR;"XV]Cb1NJXR#Q2#T8g! v@%=N5Uƌ:tMA>\ϣX }${$2]UĆ+&G+a܌uO,^3PuM$v&k*~kh'P@ZPuzՆL!/EУIVxuT~@mC m\KorxmN.c;aw/`&Ww4!=/"D o H.@pL}eu3Fp%56Z ֹ_~5cNN &5I61}69+pL6I5$S(_6~!F*vkYމd ϊN`{z rs`q8}kc ,6/k|U2My-߾׃jt2Љ>vV 'n9aVȑ0;GQ:ޙwUd6[ӭj6Նa}jD7W·.4U7Ppº =0/ 2Eȥ} ,EV넬~b`EHV7<:cSmYy1yY:t8Bh…jҐ;9cˣ4ÚGl\%y(IyYp [ax%KLݸetJKc ]*H{ЛS; , >T[X/ٌ=\y #rg/@I8$Gw ڣ~(Uj"HH~Mր4@›IKn}*cbv۬KMg, +I!m\H2;~BaT1xq\qGE*O˰I f}̛y V(僵nW&0U$kךoJzb\wW l|“_d8P epN(]h9d+1!pǷ <ÃM8hd**BxqryPXzw[[tuD5ॅGalxRq}PgYd** ҫJ,-uNΌ|׎*<"dnGN_;Dy,s-S[;lQ5[a`Rdqg0;W+(ymsK8f-%b\äs"\XS U  Mm9+2%TjrLS~5Z_HK dej uѫuFlL녹qfL::9NDukQ5'e6lFs 00 NpLiz O]SiN]Jvͯ,9 uԤNfljyIw9s2r]iQS4 68ϳ.sb b)se.wte,af s*:ނP;cMĎ`K ǚP,{Ep_oB0}J?~w~jЯ [5onSe[yGi }E"wQ5`{Iϡ[ yaNu9ZMȮ"Ah`3/O%Aiܭ_S5B|&)(vtr6>UIߣɵ?eiycE !kYfND :8ch'E}dX+a{C m1(^Uv#5w˻k5c ՘1,)#.vkpU;C?1g.x$!WDvSQS #ҍ6>`L d L2S +|vͦX:Lg 8& `iCZ]ױByhmwIԙuݚC;xD.B %̓fōJו%!&o!yw!וOPN]z$uA mp\iN:ڂ_NL Ղ{(/UҠ%G鎗L] X8 -ԈBWwy#w<徣L*&P7n0tILwQQ`yMFlp2$9y,G_w?*V?j;l *1y~i@8y{Qmwm<p!t&cnuw[D[ ~Zd/J޿Ɔ7A/rP8=a}{7وq. s6 Rgll 8C!";1vqyq%ާ| /sg6[EKNF|kRc ik}}(~ Z*(Ek֩HK26S"gna *=Y0` 8h]+0>EV0TO0np#PniNOj`3N"= :u!"`I@*Ǐ :'76| "O0 !'/~_h*F'%2/*qD($ Ϙ;t踗A@׬4'-TќASu~I)"Gn\D~yU5~2oMX&<{TRSG5:0nX#dCzRf+16~V7e%&k:\REYe~UűPKzmܘ/ZvNSEĸ,~ ^9ËюL!B܆'cwݢA8DɅH L4o =odN.b1rkZ@gη~FMg˸vG~SׅhP~ǃKSO Uh u#o\lSZ5/ $* C ($3 q@O\^:7xsprG<xW$ƅVY9bɁ̄):眇 m FdIE4m/8+p{vm1[ N kWuP*;49^ŤhnV5I+ֆ{O t.3A⠜&Fdp<#[Q]N݇4QG ׻/ŎZ2IK'U e BT̾ @{cڲԿ ?3te0a 1",R0KHF-׋;gRRCY?~H$ )@qJ:0z:Rܱ{ԥHwM-M.^_;Rr}F GL_1v4. J!dkć eDCb _\Ithc霉WIl <`sd=̫ח="kr|NSe;|+u~}rU_bRxPo҆*q^V~rCzJcQ}V~йtwz2^ൽ=agp 63a^hs.7D0ܗZ~F六JЪw$m(yl*3*5򠷁/ingn ` ٨d7.] \!_:|r5* ΖOW~7oic0.>*WբNdꄩy$] %3SnԗSN# Lq.ı`#@ Iݸvy@~Jצ'h5(Tk̳Bjx zV"[%l/nF4-hvFo5%?v67Ac`C屘f@311j=}-H3x d`);dyʑ=RrƛEsfq;4Bͱ񭝣m<^/f~ij(֎_t.㔛4x }^Ő| _A˞&%ۀ[O\@BL#rz{L]N4IϮ:7TRC!Y:&vyܐ D䈉')5M|wZ^' /WQa%Tb W:/ Y!)p kzk9 #Bk#a$1Q.sk2o499Z+k'r>oyT qF@G&E|1?7W(H&n`TTwJ(Dʫ'sEr_;g3cA 8 lh`LT:˧٨QePwo.罪 J ;ѡXg~[Ż1=&ĺ8 TJIVFPgbg$jp ZB"-ukZS Has7;H蕄@ȺbjOGp=9(Hnsy\ (/{Tx,Ϳo߼}۷.R<04윁zev܏3U||pa`Ry 7jL_AGv^}}nJIm 1ZϞHu4ϰX-0zlk!kμVw`vʝI@OJ{Ռ E dsJ80[}!s72w1ԒNb) RWEO? O;EZ^Sz2-]*Xķ䁖 ];2q.`ME@0^C*ZժFjj,0+Ăta{1 n|M ׬ChQzxo!=tϰ%:iyc]](䆀FVL(+ *㰩N}Q's @{=I|^By^ISE0&lЏe:59GgEWl6 !&=Į t )k|q;%a(nnYu|",{kfڮA/:b"kM|$i`lm&0wP-lEb#f RgGaU '9<塠mQwġP(]򍐒٘+Lʔבꬮ`a@Bl M N @CeDppϼhzp JxN|?ھrH'Jy"%&>Fu/h9nnMs` juY0{b "q]82W9¨^_EpP$3%1 On0ΠrC=aulX$hZd&{/3s`!I5Ho <顝4b^ZfQ woe%2#anfQ.6q1J 2Z U ^Kԍ+YEh0xİdӰ()Rzk;6(W}ȠFcy$L W̥eq_{L>Aq)}ETLv; zw*o">mudf B$d&զ!͎}.ۈl;'=>xN5UjvZ(+b/opN`q)hM>R; RKvʢ bS ԝU{HuS&5 1vƴS Q#do{J X'f l.H0cdRَ#^\sW~}1Cuia4%W7j?Qt0~|vN9; 麍VAޱAȯ}&~41p?=030U.`0? *&cz6I&řwZ͓G=wAB4y&w7vЙnHH4Pb!"O8qW632f.6sb?pS0 Cg O83LlxlQf}X`3*+?C:9qUBSswux0[QmgHK@r{GR\+͜XV;MTֺz6v$`k]K"94lIpr"9oQ- 0 Nmlg .Z4<;R?kc ֱvb8rlmuQ*\xyKDXq 4͏jQzd űȕ@tT g Kt;]ƿy*vJ< Mנ'jл܊@ag4YPhYx<&wal yNQ!eiMy51z~xY +ˎwQ:4>y(PFoʿ2!c~;c_$AX)AȻTa"Xb*d݋"oW@P.%PKR1Q>'kzCh§{?b)ze!h3nbd>p} <uy*i$ a zu=OW2- p2уު+Π;U,@4HÆ )bP~8ǮГ+`F0o0Gg^ r0Cݔj&iH)L!| u&qxJ 9H)DIZC\ϋf[%9eV]%tDl{݆>p> sCHu! WQ7c 9BS;t\>,T=X[D;0§eGuI{" =R?\)M: BZk&E D:myҤYEeaƆ]2t^;;5hĝL8:jbl;s&`H4o*–yP+zmꧠNw'7t=wz#'锭5.Tz(`˻S$GK٢t:,`|(v>73ux\ׇsp6c3'o|G^'c'pOG:q(-pz~wJ$Q' 9'-腀DNwpr5ƑQN0DH \睤5)5g\M+e:~'X4abs^[a* G!qȠɊ×~B僬}$Q~ԩ빗u˱أx%ʭj;8)E`eMFOOSQ=``D4ʀVҘOh [&Ѧ`_Lr [wx `)MHR!(Gib6=CDO( f c>Fq1'ZbM7PDt1k18ܭS"q6 ~E~\WHxX_7jPׇ˝usWfTlK4Asul+Nf8!YZyl 2܋`a MoBGyZm\ˊ#ƴKgĤwzlL;᭯Ecp~aHTes&;Ӕu \7*k[*5rjX iJ@70Ob!n-M >ēAZ˲Giܒb6$2τ8M;} 1k0ݪmcl zW%wR@me%RH`T!oeO #YVbfգ4W'p: b|g`25BOޔZ ٥QBf2_=hIsv ]ͫ{IeC7Y&1Bwot*M5WdQ ƯkxR;YL_bz"1dYwN>3Zn@ 9|[7,̡X@HdJag.|<ޜERt8TfNۀCO=![nQd>)56WQm^Nl/$2XSWlиD"t,# +c* ޽BIAm8 gsGrL+n h^ڡ^kwrcr0@plG|HsHk:Ho8W pB;ۦKzeL$5nf^`GY$IKz4ԕdS; ?c)60l_BGEyӑ0 =F[V{de*XMNi?Пɗ &*ώbJBď;n9P Rv0 ]4# $#0qiC4QTҝq~B'|'Ϋ΀;i<β%u (=:/8?w6?{= nzO#g3׺*&Uum=k&?2@d qnL[r#UL)Txe5$[U +dl;'@~B ;OLy$<]hnStwz#l:g4#jhx 62 19ۑzl,MO: 6"XPYwԖ>&X9rsAv굸)#]#b5k.ZT)sip<)98aHf "C݉0.8Ov|p+ ,LglyGtkkWAwyȍ8lSPST/XL>22ME/$ ;kYY0Bs<|Yg4=)'NrBgcy+CkqZjNhDfi 6&yB~t ءq1"d ܖdHi"@,w\? eL>ۻ鏉#6&cDh1wۄNq];8'*DM }gaL,B[7]]0 byau${:5( ,-pMʹeEG%zfsU7Ut4|VKs7ӫ:r=}Wfw a-@D 5T;@ۜ-Qq1 U5w[ge('ȧFO>U G4'(_@q_=2t!FzHDŽMͲSQVMn*{CpEۿmX: ;e{Nz펻6砫=pp 5#'vy.bi'>ڵzh猥8Mʠo}N9.h$$x@v)0{eΙ&7.9& ?Ϥ4=`%#~=(V^U)*=/NEo XI h6V?J9ĬŇ&+434_T+ܝ:jd%Uu"F6NUs7DbǞ xX̌Gx +/S!|,lDZ[Mc Zm sĖ|A>8=14 hośV5FY%5lmX{Td/IѮn(.=s ;tL3o%yRlFL9F 9EU0͇^Qi:blME0¨=VJ9\`ڌ=z?;|,#$=7iYl/XpD0mk9 " b/ +{. z555R+V:d󆛯 &14Metf*`7Ȱf ႛvt4S taJU&rD6PXQ8`ʦ7$MD)68@q?O 2w,gC8# S׫?hpl;Ti'H@ٔPf{=&QY>#rUmjҐf¼@w4ۼTy7r$VIN띿xأ cB%sus PQ-UYZ3^1#,< ;}%>Q$\V.+ ̎)ԷYQZ̀w8úH.< xt=kf@,+ݰzvp0w[ NCG\:hٺ0|QDi3a &Q2sL>`}|EG1Ǔ(K x1t14 4݆z.*$sB;;-@\cWEyQ Ovd3>:|yzl$W7n^4*<]񝸦Yg$5w[gG1 P{{M_,eD>b'Х1WdB*Lwui+F?\*Aɉ \ V'ڝ(uJ,A\#H'[k[a^J [9 Z{@oÁ\tUӃX_?]kD}VH,$Si2v$>2OFf#3t@,?n٘$5X1x][:ΨtJ!]-E'q= Ƞ2h4\II :˜)y8x͘Tig#5]IF$DSR. v &:a EIv:7iH%( ! .ԁ4hN2[aH;RȽ:iλ1!cP.$*E(̺t][jL $ c ҷiAN>NHP6dAK6KC%%}%=]R Riw}i:QcKSCp$[~8d8 OGgeA/ e^nqgE^2 J-n\j'Dw ? W0e$Kl\&|^!naphV10C&/ޒcdQuӇap.$j '=uov˂鞦|{#*0 6wbP@ւ:P S0}=gEH*ueGg P3l:"vǭ]k}MvSoAb^--7_i[jm7m>/93>] I` Ĭ>sP$ FVĈ! C' q L}Dg!RKUCkЎy7F3rZ1#_G$# xp?ps|tkάKuC?x2w4,#l uW KHCX"zd=pUȊ81a|BI8CC% k~:3fshϺ΅ul8C xScl\%1EVxCӻz<>9k- xPrOjdaI^Hq9cڠbbL0tʡbn#XXԂCuH?Sr)\p.ݽ4{۝ A\d :ޏNSjq xq@tX򌊚7[({x&b'vײoX 6{h"ފyLq ]|uzax`#Fp*){|l{$X.U{9AN}aVt$ɡC ^Op}I{nU ?c4o8}R"FvY씢ޯRKYq9)-Wp? L,Q3 H`8@~40cĠ8 k291Fmw1n Ȼc2; &BKCE2IgY QXj6<yt 4I;Iǻ 5yzБ{{s/ lcFscqip;u26wy Bv`CI"%N4{CQ45F ^c[;kxѱ*1v *-v!Kl^˼]WQ}{l+J&UOU1;pF{S, $at٧a8N}Ap{pl6WލMo6kiH \@) 'y͘HV]湳m6mۮm(t`]P:3nf]Тh?4ɮYK;t@ڬ-Xy31/{L ~GӇ HQy2$I߉FAV'Bر1o:c}+&fEw;ӥB&u[ TDfh|w0+` *phԬ6H ܸtBv _va.hyE\M\7b]o0`?tϻ#>gk7P.^^IrT(1.P@G(21@6U'@jc[`0칚qU[\gpYl]nwȴ)(YSW,m-3]YPEy`i4¤rSi\dfQG2މ̾eP rW{0vR' S#c\|7ĄG0 8~ Ȼ;3tSlLL0,y&Dn221m_89D?'(|:̧vjr%y2!\̐#GA懭|Ƃa=8ʈi<1TA݅I;UTZlض 6uJ.ل?@SDfVHPDCZ8". ?pɨ˿t&IE&vUMQ1`4! 04s}WM1 ԋw"Be:(HfyF9J. m\$M&=ӯ'S Z?$huu!\]صtWĆtin} `{쀸-}JH]i0Ыp0E5RtF{jk 侽sv3C,XZw+^I 9Wz 'ޮIGp{*)̷K -pQaP3b#2kSL=\Q%x2 #kX;8:tΦ}1}Ȯ-؍FRGoijqe6WI8 D+NOi0.CvPQWlx| 8c̶bWkG+]oc.lEџ{ !Þns<!/l闀wQžI1uCElrȊl,UbLT|t36 J&P߯OH|J0ygg Sj 4u 2`a\!em F1dgݜܻ;,/%f4%^4!kە(F+amw2<c:s=.4FH*2h3ƥs"g:ztmvW)ezҹyYD(ڠ͓nI\sT1bx?c[qr>$Zbt"Sݔo.ΣI2ϭ?eY_H( =<}Nާl$m"L-zB-Sq<taNc{EMQx)êʢfCh>ۀۉf$Mr\7ZzEI5H"ql>?:-Hp\tFwłq5${B/)<3A&LK6b!0qgQƷ{ԃǵtGSp1 `xb8lV`pFH"MR4R UyG"pXAG BiǎXGC;@'~MwK\ܓQ1Pki뙥*D5yӔ`M)V 9ƕÍ]AdRoFي. +ԁ\Rf9B WzM7 I8uÑ S2zK0U’"2{iU~9W;#Cv}3r|4{ >vh3ǿ=f `3D c/G|r9X -6hf+؁ a|")jXǚFb = :K_ J5b!3dhBLΝ)&iWP-b⤘L UX MN.V4ÌqOGds1O6v-3] jR|X J;'ƁgF+^94(D7lt}SF, >wg]Hv.&?M OlC?Cu\ 1Y?u":eis@[4c7:`:N)_{Y{rsiv 93; qcwX;]4Uq՚ *3 )1l8k瑣$H&Nz1;(~JG~1=(:82ӂka Oכ"pTL8s<n%Ykدo8!éf/c>$ Y ^LD.\Kݧ2 @:3jcž%`ւ67\#)[#-޳'Xs=?NzsjYt:w6ZQ[K,uJPdu|-Iz٢߆oWzg] J`[ܜ'pq㻤$ACV5qM 2?2ߡRtPmVi"sFMFRʑG 8{?mv@(w%@ tgeR 5..BP>ɩ=_` ^&V=Y{dp9qHH ܀A0LH{ӄp;Yɝ.1izkQڅ>:I*ݪ{TTj"`fhVxh0&[9Z)mqwckgrpao2m{ґ'+d("sʀmcAxE\ˏN'˟=tqEćfs0g]Mv~af hʚ1FNwd =],Z6RQ = 얪D/]TK,3OSS:syDx76pyU|+cF%C FRnWd%+<;SZW,QX.R7+hߧݭXv|[%k:qE10JӼQCOlcu7>gxz^юˠӫќw{Cc(:Ū|F;2Ⱦs@גoM$ˆ鴚  uM64:g>3Cq8ޠJbO>l'-] ❟La=|kg]J (>(W}N8o`jZdN,Lis I qyc9R5l?RמHѱ4$PTO$opͫ/zEä(>>indq)Ƣ'%OtR54ø;];(nrvg&E"NwSgz9tizE~Ì"n"f9ϼM5 m ʀv<{;&{iɩ wᘡ1C{6Wb*L* x>vAUv'#6Qw٪Sm;169fT=KUuwY\;@Avg,J-]ul'꧵[o“Bҽc <ȸa529|EѨY$=\ƬJo:NKp1C, 1> FnY~brw(: B$xBT@Y$ tjZݞ:I6ݺIn+Y-@So<9M!y!r U?O$ڠҏ1۞  RGw  52 IidN ^J(&qqq !yJ*ʦn2M#|f(?"^f"tSR춥LQQE]+IG+{_GQM$xFz^"3t%G;MvWO;AD@%Vtlw+MKNe%r#K0d^TSvu}&=ʹfFH$o陗j/OggJi (]6;j@"s>}d! ueJ)Jt>\=b_,-bkTy'tRBG'=wܱ-dKEA9`B4IrNL~|p*9IЕ>bsnS¼U ض8Q疺K-a< ?=py<^GㄊP6P JLA^I3p669J'O.E okrRa(>;T:7#HFp-RsW"#P'(tFA}X3nO$i1:oՌs@3'Z]"s]B?2ip -!.1&z֧/ -E[,"y4b9H<*l(OwS>szO~<@랒Oa} (!D\"lםFҤaþ*<.?RL6 N4u'oBe0!s/E!$L_ϓ6Z%vMA%hHPq,X2mF'1%206 my&Nf?_yzI!UbEl7txRi"OC xs's"y8QNSFI>h R CvAveeY|0r%mxƜySEgxrEWAi~Kd 'nJ%?@$Nqα.{P>Bl][$>ao\. wRhjrsk< `D7q&1[4>KlEayt|.35CYK~ʞ_إB\UP\9DrwÜD C'7qJj&4B)NqA=%'r0" =ovq'{z0, =GGE+9D;* lAC tBR]#*ҿm.n9`pQnBtO~edxhTVY1ϟףo@![ , F{K'(`˰˜VpS'OpEnfsEScS5{pJ L'iH [_\%1ȄUF6n˵Y$ce:%}/(&Irw#x{هB1Uz5(T F>#Ɗ>LBus A fq EzƁ2{q\#ܙ>0s#^\sW~}ѭhm~钧X"Ů4tw.yOyI'm{vJ%?uƵz@TL'Yƍ#{8̣Mc7ը:{vfF+Yx6x2 %9'ֈVyxk#ys#0٠8Y9v0Xk&Aݑc pKhgm\\A-p0Ǜ"yY Nj݆Y:wNgW$TO^tj ψxxXfAaeX IV]i4G8þCC`x.GX |/!B3kY܋2GPT]Bb޷=AE6]3Nod%_H9t8DN!ʠlİǵnL>AAEh1|Pw(Aqg3]&[MiRDـF]cT|tq=$(l+ Na項8A\T lNB)ӊ%8'5([q>5$ B:}H :/pՃM#g%[X_0􏁣9A fW8ChH RWtmdW/Ǝ ¦ Q1>O6-\\+t*"FG3F@^i"n$剩?d&u2;Ni\՘ 'wL~Xgh>+j&vJDfLwjU^3|ßV5gm_&+4ZmX%-<HWa; py2*:wI,Cȁ&[26;ԇۏ3䮂4&[46g`FH*r?zRuMvn"af{bMG0P9,[@q2x7/v' L]"Jx\/yPG!cVY]P/)iH+a {7ehG}D<+m3~<.\3ODD*sbXegYLWEs^="k󅜱S |XzMC^ !e2_(&Jב  ~ijQIg'z6ظYγXWmzaP&&a6$h&S25}k66'Z N=OrZLPQ'98/=ˌ;Fa;goxob8X"F蒑q@N,h,ЛqAOt|ٺ-"p(3JmeRCYw|eh%t\ K6%Jf678IoD:ﮚq4KρpJx#w%P7 innf|6EsF 'tLp*0lItw6d.>Bȷn çGzl*I5eC`/[ʪ$KČ)J!- qU",z$u# 6ZgAa6HٻTc J$5-P>, EaB'ޯs7T0d%Q>P>Ye?ԸD3쉃}vNDh0p@oBu}͓fHR#R>ZUt]BO7F"Q(ZmC)㥘ę$^Irb 1_ɧ1/R9z"KR.$pߕ} L,658:O v't|Bއ&f $Myyd=a f^N_i鋧557u&07$&1CbTUtRUҺwFf6NҼsr9hr:bDNt Fvbh.ӥH.)۵iE|\*μ}WC2ZI>əVaTӬ$K6Z䧰(3}S88pע)}DN8# arX -&& ޔjxH⌂RS4L9wҲ:78qi4M)GcL.Ƭ$^`ܶ]8;2:|ãW >avi"8t5N T\>vL dG|&c<$,dkeʺw+cw+g`Hv" =#-Q6Awk#8cbPd,41``^N_9YsX3= BI F;Ai6h`2jLqڨF)vG-L);[sZ*Eׇp'l]\t=JHG.L gu($o?}`;wR)gcyI&AM4adBI8 +҄=^k7-sh();Cs۪Y Rn*Wnv}F#\>'MV.j vg!*jX=6F  H%px߃\"sGi zQ.!ty|t"^i~ w l׆0CBBUx|٠C , ԡ]@qT2 #c['CS^ȶTik/^g(ڃ*tw%A )Qp˩=Bձe@(mZc8$1"F$qyޥin#,'i-ˇjނ H9LLކ8:uC&30{|o&4qfo4 TY5ԙ7neҍGGMe$U:_;6aZP4"zNjTsDa=M#dgUL qq;}{E'Q(Ա=$~OLInѡ~.y}/# X\wTL nHI ֠>bJQ9ɁVCQz 4@.,;nCcdd)<8pZ rgf3c!Y慳gAZE,7:l.pdO hY&ٝ5LS`Q,dD@NtuQWSLm68Cz[bgxW00V ]d:"V]]*&wyQ9KS6+ԩq-Ǿa]<!4 3tDu`dž} 5v/zd=qWsUsl}M ĤÈ)J 9}{ Љ2CY+uF=`C"ZKѧscXNE15/ \wy9i@N%`'W5 }>R֖ ,~լcPtwyJxn[Ćr"ؠ F03&<\gRQp ί+t<\ l0C o i*Bbq52f j$ ĝzs`clDGnϧ_GV˸3{tt'uHBҐ:W|u9{STq܉3BAfϨ Hx'+idĚb:8Mf:e>żl8Ehe Fg뷠^inUnlr~u|ܟ :'&lýރb@.5ŻUpQ^\0#MP 0ռA.J%H>0jQ@ F}䭆_ ;=5ZeT{sӔh# $yܔ|z+I|)<6Ί{v͇T8A(̓uUǩ?pKCΎ}Z&'~rYUO5>:lՌ1%f!g*ƒ}SJ1[a朓\%t=#xco0Hj%jXBw|QoBO'r@bQc2kFaE!/<2i/ѕ{&Tl. 6] ĪA3$?Y &c0; wS0`0 ZBwt 5@T~sLFC>2Z7'wۂKڂ(|,){Q Aw B4akZT&ԤNg \/kTzOU%U?g*Q\` +yC¶(Lp:$~MRP9L8vCT5ϑֆ;(lEe\huzSsb-sg&2ŮN'+n6Lbu0 rLj_yqxpx]缳JB[ H32y7FD<(10a8!ZE\,֌v͂:܍ A)&!/t* ƘrX#SCF*%xTbq [A/%qhx%;WŔVMOu5 фSӉi&YRK. txC\mVIțWYoj;n|:j<V=n6-G 属23jIÙ3#7W>zl0'=ZJD<-&elqjL9?:#MzA|s6MٸxKz[y"!PFq܈hy#?[<O1{lΚ `3vͭwebg,s mUkMnqX6Z[`KqnYTaٻa?E4aeպ5 8riܪ5˵V/]LBKgF{}1doL^UUtSկl6b1c EaRjOĽfe o;*C㎜_6B9aةLM)d uDqsc6Z)!eqj$+݄WxNۮKMv9RLmlAa[Fot|l3=% .mwC$Yؼ*VF\/F=brOܪ@m{vF;hO{sl#h~$:UiXTBצPC%⛷ҐBztPLΙ >+';ݻ>:j Hkþ+m:bEN P$$ !Z߉݈&ws:/wy6Zjz!6gH_]:gJ0BԝRd%juRh{'ٜ ]q- &q]H!`A\uOaGboAM UH]c`Bۛ~3yh4f%f%YI@W43v4"^1%4sLYo^Iˎf۸ۑ!f2gF6"3Auh~*>i绝ok]oV|47) >f]G*զowa{uWM٠)pٔ =ɫ.|lʢrA/\s.w o;On^ y$%BG^2u\ [DW2k` hÅɜs&7Ttc27t9ɇ\G~CGF6y-nOv5%0y!H/kOSϩ7RzΥy1g)3yLL֞<Ū;"E8Yi/TvGŅhK*j YJG%MlۚhP}`6[uOa/IUrսZP,Iїwzz݆k+lY'RPUNtS2T"+5=E-%xJSqӬVmj2 hK^n9&e.k'f,C@f:{M X~xN84؉fҿ %msogP/͆ s=,8IBt_`XRpkΆi<덺9VcTwÉ :]X_|< >CAN]&Oׄp4I[Xc~9C.[-P"cǃBo {1Q@|BD=ȰSOwa1OW%"}4 ZDy"ƕqom0 /^:DYǰ=k9 5EmxPj/t611M^{~1wjrFX{ɹqe7BH![$+ilWAf!qc*(iG8 ;ʩ=f5A aΝV?V*2ZKZ}߼vÛWo_ NMTm:\%bG,EjlŗC 4Im#_c2A lØ s0k?`t#!.ݸ`ڥ;\YҮYmvTo$q^*(YtkY/ Q[j"MSN" uczu SL]zﭡO~OMkkĉԤư:.&z*ܼyo>hXoo5Yݿ_~=Qb;aomU܃mqD8oN7r018Rj]x-Bah{*)>V{wPuLC(b{ Z8a׹p7R903@|$ؖT$Z}FP:rpзI_D6Coմ<#zFVJ0Tq5x=:z(ݖ"Oȥl.juSPp"~EܽV`OFצ׶=ѝ̾h%kg0M ==Zlv- Advanced PXL Script Functions
advanced pxl script functions

Over the past couple of weeks, we have been covering the powerful cluster monitoring tool, Pixie. We have covered how to get custom data by writing your own PXL script . We have also covered how to enhance your custom script by using data manipulation functions provided by Pixie. Let’s wrap up our coverage of Pixie’s custom data collection by diving into an example of an advanced PXL script.

A Quick Refresher

Pixie is a cloud observation tool used for gathering monitoring metrics from your clusters. It provides a Live UI which is a dashboard that can be accessed from anywhere. It also provides users the ability to execute custom scripts to gather custom data.

These scripts can be executed from the Live UI or even from the Pixie API. The language these scripts are written in is PXL, which is similar in syntax to Python. In fact, users of the Python library pandas will notice many similarities between PXL and pandas, such as the fact that they both use dataframes (which are like spreadsheets but faster).

What We Covered In The First Blog

In the first blog about PXL, we ran the script below.

# We import px, which is the library we will be using to add extra data to our table.
import px

# We gather data from the last 5 minutes, from the `process_stats` table, and create a dataframe from it.
df = px.DataFrame(table='process_stats', start_time='-5m')
# Below, we are adding extra data to our table, using `context` or `execution_time_functions`
df.pod_id = df.ctx['pod_id']
df.pod_name = px.upid_to_pod_name(df['upid'])
df.pod_id = px.pod_name_to_pod_id(df['pod_name'])
df.cmd = df.ctx['cmdline']
df.pid = df.ctx['pid']
df.container_name = df.ctx['container_name']
df.container_id = df.ctx['container_id']
# We group the dataframe based on certain attributes, and aggregate the data.
df = df.groupby(['pid', 'cmd', 'upid', 'container_name']).agg()
# We display the dataframe.
px.display(df, 'processes_table')

This script covered the following:

  • The syntax of a PXL script.
  • Retrieving a table’s data (process_stats) for the past 5 minutes.
  • Adding new columns containing context data.
  • Grouping by certain columns and aggregating the data.

What We Covered In Last Week’s Blog

In last week’s blog, we covered some advanced PXL functions used for manipulating data. These included:

  • Joining Tables – so we can add data from other tables based on common attributes between the two tables.
  • Dropping/Creating Columns – so we can add more context to our data.
  • Filtering Data – based on columnar values.

This week, let’s dive into using all of these at once to create an advanced PXL script that gives us the exact data that we want!

Creating Our Script

Let’s set a goal for this script. I would like to get the conn_stats table data and merge it with the network_stats table data. To do this, I would need to merge both tables on as many unique identifiers as possible. In this case, that would constitute the time_ and pod_id columns.
There is only one problem though. The network_stats table is an aggregate table. This means that it collects data on an interval basis, and sums up the collected data, so it can be added to the table. This also means that the time_ column in the network_stats table will not always align with the time_ column in the conn_stats table. In fact, in order to align the two, we will need to manipulate the data in the time_ column to match both tables, which we will do later on.
Let’s start our script by setting up the base structure. We will get both of the tables, and display one for now.

import px

conn_stats_df = px.DataFrame('conn_stats', start_time='-5m')
net_stats_df = px.DataFrame('network_stats', start_time='-5m')

px.display(conn_stats_df, 'conn_stats_df')

Setting Up More Variables (For Context/Merging)

Now, let’s add more variables to our script to get even more data out of each run/call. Let’s start by adding the following contextual columns to the conn_stats_df dataframe:

...

conn_stats_df.pod = conn_stats_df.ctx['pod']
conn_stats_df.pod_id = px.upid_to_pod_id(conn_stats_df.upid)
conn_stats_df.container_name = px.upid_to_container_name(conn_stats_df.upid)
conn_stats_df.container_id = px.upid_to_container_id(conn_stats_df.upid)
conn_stats_df.namespace = px.pod_id_to_namespace(conn_stats_df.pod_id)
conn_stats_df.node = px.pod_id_to_node_name(conn_stats_df.pod_id)

...

We will also add a column that will help us merge with the network_stats table later:

...

conn_stats_df.time_aligned = px.bin(conn_stats_df.time_, 1000000000)

...

Note that the above function is advanced. Here is what is does:

  • Creates a new column called time_aligned
  • Bins the original time_ column.
    • The binning is done in one second increments (equivalent to 1000000000 nanoseconds)
    • You can think of this as ‘rounding’ in a way.

We will create a similar binned time value in the net_stats_df by using the following:

...

net_stats_df.time_aligned = px.bin(net_stats_df.time_, 1000000000)

...

Notice that we are using the same exact values for binning. This is important, as this is what will allow the dataframes to be aligned correctly.

Merging The Two Tables

Now that we have everything set up correctly, we can start merging (joining) the two tables. This is a similar step to the joining we have done before. Let’s add this snippet to the code, right before the px.display() call:

...

df = conn_stats_df.merge(net_stats_df, how='left', left_on=['time_aligned', 'pod_id'], right_on=['time_aligned', 'pod_id'], suffixes=['', '_x'])

...

This will result in one big table with all of the columns of conn_stats and network_stats. This includes duplicate columns (which will now have the suffix ‘_x’ added on to them). Let’s take care of cleaning this data.
Also, take note that this is a left join, meaning that all the observations from the conn_stats table will be present.

Cleaning Up The Joined Table

Let’s drop the columns that are duplicated:

...

df = df.drop(['time__x', 'time_aligned_x', 'pod_id_x'])

...

Now let’s rename some columns that used to be in network_stats for more clarity:

...

df['received_bytes'] = df['rx_bytes']
df['received_packets'] = df['rx_packets']
df['received_errors'] = df['rx_errors']
df['received_drops'] = df['rx_drops']
df['transmitted_bytes'] = df['tx_bytes']
df['transmitted_packets'] = df['tx_packets']
df['transmitted_errors'] = df['tx_errors']
df['transmitted_drops'] = df['tx_drops']

...

Of course, since we renamed the columns, we will have to drop the old columns:

...

df = df.drop(['rx_bytes', 'rx_packets', 'rx_errors', 'rx_drops', 'tx_bytes', 'tx_packets', 'tx_errors', 'tx_drops'])

...

Full Code

You can find the full code for this script below. To give it a quick test-run, you can try it out using the ‘Scratch Pad’ function of the Pixie Live UI.

import px

# get the conn_stats data.
conn_stats_df = px.DataFrame('conn_stats', start_time='-5m')
# add contextual data about the cluster.
conn_stats_df.pod = conn_stats_df.ctx['pod']
conn_stats_df.pod_id = px.upid_to_pod_id(conn_stats_df.upid)
conn_stats_df.container_name = px.upid_to_container_name(conn_stats_df.upid)
conn_stats_df.container_id = px.upid_to_container_id(conn_stats_df.upid)
conn_stats_df.namespace = px.pod_id_to_namespace(conn_stats_df.pod_id)
conn_stats_df.node = px.pod_id_to_node_name(conn_stats_df.pod_id)
# we convert the time value from nanoseconds and bin it to the nearest second (since there are 1000000000 ns in 1 s)
conn_stats_df.time_aligned = px.bin(conn_stats_df.time_, 1000000000)
# get the network_stats data.
net_stats_df = px.DataFrame('network_stats', start_time='-5m')
# we convert the time value from nanoseconds and bin it to the nearest second (since there are 1000000000 ns in 1 s)
net_stats_df.time_aligned = px.bin(net_stats_df.time_, 1000000000)
# merging the two dataframes based on the time_aligned and pod_id attributes.
df = conn_stats_df.merge(net_stats_df, how='left', left_on=['time_aligned', 'pod_id'], right_on=['time_aligned', 'pod_id'], suffixes=['', '_x'])
# drop the duplicate time_ column
df = df.drop(['time__x', 'time_aligned_x', 'pod_id_x'])
# rename some columns
df['received_bytes'] = df['rx_bytes']
df['received_packets'] = df['rx_packets']
df['received_errors'] = df['rx_errors']
df['received_drops'] = df['rx_drops']
df['transmitted_bytes'] = df['tx_bytes']
df['transmitted_packets'] = df['tx_packets']
df['transmitted_errors'] = df['tx_errors']
df['transmitted_drops'] = df['tx_drops']
# get rid of the old named columns
df = df.drop(['rx_bytes', 'rx_packets', 'rx_errors', 'rx_drops', 'tx_bytes', 'tx_packets', 'tx_errors', 'tx_drops'])
# display the merged dataframe
px.display(df, 'df')

Conclusion

In this blog, we have understood how to create an advanced PXL script for use with clusters that have Pixie deployed on them. We have combined all the ideas from the previous blogs into a final, advanced tutorial script. You are now a PXL script expert! 🙌
Keep in mind that there are a number of other functions you can use in your advanced PXL scripts. These can be found on Pixie’s documentation page .

Leave a Comment

Your email address will not be published. Required fields are marked *

2

Thank You!

Your e-Book is on its way to your inbox!

For any queries or feedback, write to us at hello@capten.ai