From d39b0061f4212747cf7fb4f728470b7cdf2c1ed3 Mon Sep 17 00:00:00 2001 From: Ethan Mahintorabi Date: Thu, 20 Feb 2025 00:01:05 +0000 Subject: [PATCH 1/5] utl: Adds live prometheus monitoring to OpenROAD OpenROAD includes a metrics endpoint server that can track internal tool metrics over time. To use this feature you need to do the following start the prometheus and grafana collectors [Detailed instructions](/etc/monitoring/README.md): ```shell $ cd etc/monitoring $ docker compose up -d ``` This will start a grafana endpoint ready to collect from the OpenROAD application you would like to track. By default it's looking for an http server running on port 8080 on your localhost. To start the metrics endpoint in OpenROAD, run: ```tcl utl::startPrometheusEndpoint 8080 ``` This is all configurable in the docker compose file, and you should be able to access grafana by going to http://localhost:3000 username: admin, password: grafana. Go to the dashboard tab and click service, then OpenROAD to see the pre-made dashboard. Signed-off-by: Ethan Mahintorabi --- docs/images/grafana.png | Bin 0 -> 134786 bytes etc/monitoring/README.md | 65 +++ etc/monitoring/compose.yaml | 30 ++ .../dashboard_definitions/openroad.json | 148 +++++++ etc/monitoring/grafana/dashboards/default.yml | 9 + .../grafana/datasource/datasource.yml | 9 + etc/monitoring/prometheus/prometheus.yml | 25 ++ src/gpl/src/nesterovPlace.cpp | 10 + src/gpl/src/nesterovPlace.h | 4 + src/utl/CMakeLists.txt | 1 + src/utl/README.md | 48 ++- src/utl/include/utl/Logger.h | 12 + .../include/utl/prometheus/atomic_floating.h | 77 ++++ src/utl/include/utl/prometheus/benchmark.h | 94 +++++ src/utl/include/utl/prometheus/builder.h | 61 +++ .../include/utl/prometheus/ckms_quantiles.h | 224 ++++++++++ .../include/utl/prometheus/client_metric.h | 126 ++++++ src/utl/include/utl/prometheus/collectable.h | 48 +++ src/utl/include/utl/prometheus/counter.h | 142 +++++++ src/utl/include/utl/prometheus/family.h | 391 ++++++++++++++++++ src/utl/include/utl/prometheus/gauge.h | 171 ++++++++ src/utl/include/utl/prometheus/hash.h | 78 ++++ src/utl/include/utl/prometheus/histogram.h | 182 ++++++++ .../include/utl/prometheus/metric_family.h | 41 ++ .../include/utl/prometheus/metrics_server.h | 58 +++ .../utl/prometheus/prometheus_metric.h | 51 +++ src/utl/include/utl/prometheus/registry.h | 153 +++++++ src/utl/include/utl/prometheus/save_to_file.h | 114 +++++ src/utl/include/utl/prometheus/summary.h | 180 ++++++++ .../include/utl/prometheus/text_serializer.h | 264 ++++++++++++ .../utl/prometheus/time_window_quantiles.h | 90 ++++ src/utl/src/Logger.cpp | 37 ++ src/utl/src/Logger.i | 7 + src/utl/src/prometheus/metrics_server.cpp | 157 +++++++ src/utl/test/cpp/CMakeLists.txt | 1 + src/utl/test/cpp/TestCFileUtils.cpp | 69 ++++ 36 files changed, 3166 insertions(+), 11 deletions(-) create mode 100644 docs/images/grafana.png create mode 100644 etc/monitoring/README.md create mode 100644 etc/monitoring/compose.yaml create mode 100644 etc/monitoring/grafana/dashboard_definitions/openroad.json create mode 100644 etc/monitoring/grafana/dashboards/default.yml create mode 100644 etc/monitoring/grafana/datasource/datasource.yml create mode 100644 etc/monitoring/prometheus/prometheus.yml create mode 100644 src/utl/include/utl/prometheus/atomic_floating.h create mode 100644 src/utl/include/utl/prometheus/benchmark.h create mode 100644 src/utl/include/utl/prometheus/builder.h create mode 100644 src/utl/include/utl/prometheus/ckms_quantiles.h create mode 100644 src/utl/include/utl/prometheus/client_metric.h create mode 100644 src/utl/include/utl/prometheus/collectable.h create mode 100644 src/utl/include/utl/prometheus/counter.h create mode 100644 src/utl/include/utl/prometheus/family.h create mode 100644 src/utl/include/utl/prometheus/gauge.h create mode 100644 src/utl/include/utl/prometheus/hash.h create mode 100644 src/utl/include/utl/prometheus/histogram.h create mode 100644 src/utl/include/utl/prometheus/metric_family.h create mode 100644 src/utl/include/utl/prometheus/metrics_server.h create mode 100644 src/utl/include/utl/prometheus/prometheus_metric.h create mode 100644 src/utl/include/utl/prometheus/registry.h create mode 100644 src/utl/include/utl/prometheus/save_to_file.h create mode 100644 src/utl/include/utl/prometheus/summary.h create mode 100644 src/utl/include/utl/prometheus/text_serializer.h create mode 100644 src/utl/include/utl/prometheus/time_window_quantiles.h create mode 100644 src/utl/src/prometheus/metrics_server.cpp diff --git a/docs/images/grafana.png b/docs/images/grafana.png new file mode 100644 index 0000000000000000000000000000000000000000..d5c953e7630d02d4d10611ded9e61b7fdb9cafd5 GIT binary patch literal 134786 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�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

u+1e&W`Uo zuU$y;tS&P*A7VCoo(tV2S%Tnz0b#Vm!ouSDQ(jg>@6S;RiV8~Y5|~2%j~*!Jyk=`a zOpI}sPL3Pf3yj{PcN>}K`CY)QeFMp`5{vuBv}kT)k=i*ftc|ui9=XSo)7tv_Qhw4y zFtBt?HlHKtetsIyqw@bf(?c-waKp{TGqHMBXBg1tKxseYC|GOz-lzb5f9HTg_vRw@ z0ma|H2l3AGO zp!G%|B_%cFdj9Q6I*0I)C&^+vYm;x{Xg#=3$o+08DehxB|M-X~C6@1H=7WTq8myuB z9)e4TNqR;qY_U>PQYz@O{ZxbEwn_FC!eKwD4AB0?>DR>nAUe`@1lM*q$k8z2>`Ps`vQVXrTl2{qgkB@Kv?ge=_~u?sS~(i}c+i@9&-&Pd9HXaZakqm02# z=OaGR#w~i%>GzurZP{Pyjp+ioJv^AmeU@H6D~%kJ@@!QOwF`2uHD+q zCY130gqVR5iCVj*<<99OWVX_**X?p6wftxPE^*a~!-xvcl!EdyACLBa3ka+z;Wr8&3zB((Xk8SKt6Yn9$NNtY`FrItcn0l87ed;8>atR=&6qQ5t4YpNw|37s44iY1E;3CXvb+CPG*`b=vhH)bdIUb+AHClzDdxCIGZ5l!W~ zfBjP0yQg=kQ_rD|N$TbDt0Qrf^R)Nwhxfgeo+C=l_dSu?p6deo4I9M3R}AnuI*zCi z&;P7Iz8%5!eWB?Zf+yC}-0UBt=n%y$)?% zTX|CEi8_e7*5iSxp@c!lWqWej3Wv59>BDn|%T~i4tkq1SFF|Jf+Sbnpu6=A8*3~f4 zs`QW8pWdr4L)WGH{@2VasKA-0JZGb;DEjVv5o9ZrQX9AfHNgBtu-VwSf@*5GK~LbV zzy&Chv0cHVp;1;@oSlQ>u$SdeSnsElEIeUq3F21iUkd7Ai-SM&XZY@;qns>`hz8y^ z>(k2)r#2yrlPW04wfAQF1Z@-`-i~}!HXGi(QsWagRExU57YPaq8E7o*q$1&M4G-uP z0#|iw)L$35OjTbH+RmLOda>{9F9+-K%)t;6YW++tK)49A-~fR8>dJRI7M|DI^?>wBVAY7qLnm^F!8i`QWq znRNt4b*iiDu|xA64hQELr9HKDkjyfxuMa^e#zC+1$OG9q$aCC4P$tvculo%N^WY<| zIXFdf6x-H9dEqkB!kb&yhDlh`_(ZF$MRVq29&>hdayBxn-FVJaq>6I z(f9KC^{I}hSPKtJWba(Rb@h*-KAfyQ$i2AuDk>7NlGDkVq2)j!N{5Ha#WRjBwWTz^ zJQ4gpnOd8$xlb6x$qL-C3fLt&vF}G*W#(M6D_m>&-Ynnyz=4DJOH)WXUsx{zCoM`> ztszcQw&QiAWDRmd1*xUi;Kf1QzqQ8{TIdh|PGN!f(QtKH28j@*b!jB? z!?l6esCoTiKxBMsQeexCTgTWOsa8n?0S`|t_`Y5eh?v0%J}Syck5U&G?x{LoUQk2G z`$9s~Qt4S}|pvg6>p@GPGlSRzymT7mo8rPxqtZH|2eJ^m< zp@ndNpM0u%lMoLdpOV2^U|8=P-Vge4_nSSf(HYkbygHGE)~#I}SQ}x42@{cRP-VNi zT%Cg=Vz$3k0%4-olR0n_{YeSw;Hcp&O8&|u*UNeDFGF_H_ro~fCNWi|jN^6oyHIvSHeuTj9Sx1$Z5UR+a>JUBpZ`Vv zpOA(Ieg?*YBk4^v*?B#e6)4LpAJQhn*Ls7@Y{pR*(HLzI!nrQQ?`rB zMr7F8G1JlYNl07Fya{-%?fy(Wu*<~3f%VrmK>{8%&DGRTazt7J7ZbCb)z?MFRPT6r zc$PNPMbjomK$-McZb8?;fL265vt22lm%Hen9c$&P(Vhv6(RlX5v$MeLZ0r=>l(;xY z74YFmx{wH67a0GJWO97Q;FaNFrDxB{U+BF5if1w11BJL#(@xL+7LXy(f8_^~J3a|B zi6Emj)`!#cv(@$i`Y&X7+A^YKGLV^41tcr{vTDWiH+%p!S-Gaz9;od0pFgDeL-ey0 zgjwl$Zk_Qh~*W3VqWsamSlS?Q?Um2K{B#(vJP1<)1&7 z>@eggjZ6foOAU=X?@3AKbw1i;+7%r0p*Ef9>qDLwp5OfD^wXz9J*)=LH1vWFK~tm& z1mm(|f7?zm4FE4e&4MYtB}eg&(WCMq|1}q0)k4vRP^W&#t{993Vi4tRAO(t z9h!%_2T(_GW*JErbPoVtWJfZ7O~{OTX?F!RK`!{bb^<%H%1qZP0wd>tb{kWiGvmXt zb0$J4UpMf+vYQ8hXO0v(#D4q=;8(XW!I;;pHv6CzBKH5=$1I)CuZzu8@SK}c0WgOr ztg6PpXL9AcfMI`^43df={>i>U7e=rW2hj z08BFi?T<6mxe1t;{}lP#gMe}fn9%W$YJ-eMhE-F$75gL+|ZY0zyR1| zWg0^a=z~-GA3@s{@yR8!J1~!&26v3`kCS)b6vb7rleH8rj4E`rf{p*h>S6H3>w$nO z`7Klt;ZGXI?pYQ$w9;rsE0B`-kwaEVW_Uvr2gs9i%f}g=78OVSR|IXvh_OVwZKrjvtc}QPt1^3Nq`n6_=3=siY1_MG! zmQqK!Xy|cH$chav&p}uoO(;LYx6i_U#%2O-?1`Kb7r}q}p=K#dCXdvN$p*fzJb|Wq zLIDW6y2_g}D&j1J<*C6|Qm6yn$FO#*qTuhs^=LW?{vA~LA#-|G2O};WYWq2r z5H-XBHDdTa%u@Orfo9&iYVs?vkdc^7*quCbWfmpCxk88@!$&U$ys%rIBNJd0$lg*S zy%&tlZA-!~NjZUpc<(Q?=2nam;ySp8LG$jI*A3!28T4)_g%tvd0v^P5sShVn;eJVV zte;R3h>3TIIt0*O0AUu;B>jtqTLmiZqJJsP&1dR@Ma_B^8lJ!oVAN5^Ys6WxG;q=g z@#!!BA+rBEVlY9!$w3S{Em?42vOj*f4G1`V5IlxF6hNSswu1KU@dajPbpjK>mNKMR zs><*9{|gHMwkHM~2(59S2?B9cJiarwGWDZ)@Olz0rD*)pb?69^2xjPsNLni-z#9=r zB9s?43?Yet+KYBa@Oza0;gCd_^1V%D$6@2(_|R!>MDYs-BQy@X#7sz=6tpXWrb5jx z(nD$fcM~D9@a3oRxvlSQDRhfxWccBlfbrde6tHngYTqBY!y-rpehW!^f@$7CyD>PV zz7T_L13&lwV*3g*xhP+JZBN?Y79;7n1Y+spz zQ|Y<6wd$Oa&iIgqLT0mg?u8k|D@ZJaow({=0`h4-=^Feh)zY7p z5S9d-J-gBP72opzXEHS|AQuv`7+Vyl8fq9*5>h2(S>x)*D zEAct#3jc%w(&W+}J_ieV8pu@jH$)*qn|%aUDzGGJ?+>p8=#tQDqK0zmev?xz?++Rq zP)n5djOne@U-1S7Obt%Uj^P2sBXGeO`Ce2+o&f24UN{40gA|>#V&KrM@WE)h>lnnq zBxcbk8vlY7Hpp^)B6?% z2*6D2%rn>kKg4(K^BkNh8DbjNfOIwjP^|PVQZxkE!G(18`{>+rNM|9r(Advnf5v7H ze(sHq!;K1k^c3I_v4}`ygin3bL=Bvs zv>^J;|D=tqaX&=nb$#hTND!cd$YhSxbz-ozz(n+~0zqJ6D*ZV*`8Yu>{Qmy_WsMXG zVPShwv|+X)khn$=7!+Fw2{Df$IPHipFIfhjjy#S#kzj9Z0;~=Z(V~LcgQTJ9_Js{3 z4S`_zyF*SlNE!?vX-E>aLPv>80@84Vvv%o(jb+4ur#TW82fI@rLx=>H8uxG9L9Ht0 z@#j_lsbUG^Jq!Ophl1==75rlDbObtLS?}i)s~7?=sMv1)~?AUj-3B>L3D!Y&Pq@Kjxzap;U_yFAkcCD_Q~@IA z`#3U^p}ds22Q$Ki>c_Q5I6tB0zDrl^90DN5IAp)9uOcTvNV0_pO};F%9{`UCSymlj zS^o)XiJ}EN|0gQ*n#=*91HoNOVcKMV_8JrzZJ#4j(xkIuu}l6Gj^O`u2LN9H0eUXH z2g#t|2eSV&o45c`yb75ip;JCrNZK>VsDK(%uq#150NC8;;7{Xy(!%ZE`{7LpFQ1-z z8PTPD;=8pMAVy6A`%Za=6z69TxgHXqH5MQb1q5JR^#P-AKy<};A@ZS%z9oFdCJPF7 zBb|9Tj&EIqL7K2u-5vIP1wzWSpZNa`uTIcS~<`luMAH?+#W}uIc!}*ko+GoA@BhiYol*?-vKfTAw*3mkZmjw z7+5sz$E`0lV>G{zO6wtegOGhaIgIL00i&`iNrZNOm z{m~nQr5A%Cf~GQ2=c7TreGHy>fQ?O3%gRD{QbF*I?LZj@Rb}Z7_!*&^p-SPAo+Zg8 z?`e=;x2dCJO`swQ$sGU5BTLO7>LVZP4xwbX{6`3{S=;?I&Y4%>!O-|%Xblr}9UKHY zN>4CIl>;`dtbY=Yar?EJK55z_EK7I9Q(<8;`X6K19dotEZLMh9C!XC$K6qJ=5Db~x z{m#0j-xHabTELF{Df=Q?#W2V`7-wz@i~EsjPQZu4(g#K2Si->2e!vEh3?CUx_5xQ2 z7J)8{4@?$eLqw|pwkig;iki}gLwAS?e$~g2VpsxqCcJ9FsAR`9;W{QOLDPV`{%1Z*(*>{&P2A0u^Y2)cy}( z4bzVSExjZ8UZ6IA0z)OOtSsFUK$saNT7;5OG;Qbhb&QEyL#Jj61UfN)Lh!VArK+KH zvR-PgxS;Q=ZGA*1?3a8k9HbW#6dUWB%^|RODxV+0X(n)gT37Ldjlcpt594wAfS2Xh z5C5d1s3e<7<^(XGyA3{qp#9eXiAM{$Qzbi^Hc0!fxA*^6$%8$|!07Ai^LtUgDF_X? zt-IC*4JZWuuFGcA(b)r=Adn!{?b;gFkP^np*mbz1X4@eY1xW!Om{db`Z{;<3Apsis z>yvMBSW^8^r?$Ui1 z$<9|JgmzT-5p}}^qWyoy0JN6`w3j3P!2xJ5g#xB|)x@WR+6*|d^EjL@ixMFZA=#XZ zT39$;P}|whqFpzxIQF985dvLY^g`}o&pG-?-F$awlPi# zwt;T&8XfOa&ku%COClb%zUjS~<C2 z@)_PW{GI%KU?8Bl3#50JMmqRFQXK%rkM}VxWdOC*0ks5f`is6XW?lLH9vmdaI6tqb zstUmN>o69xmg4~bW$5B##3)r%Rt20!t7KY;$-Nz3EwQr&U7%tGztVmC9xA2 zn=VAf(RwFNJZEt2-2f8i{_0Y;H=oPKC9a8O@G<^qwuIIvCLL6Z0*f%8%SSgDxBjcW zF+$jU1gxq!1_CZJ(|%&wy1G>C^w^do8J!fKzB1xo`mSyfNp$|_CkG@jXQumy+EOrV3WoMTQgKwQ$Svsv4^O@C z0SgDm*z)#vAZRWbcYurkJU2c)dgtucdvoA}>fiw2Kl9RF;!ORBpJa+y5Q?=}dk>mw z!<}Ll)5)!5w}mdoGvY9CSe!H_ z0-q61=lN8Pji6?U{xevGH=EW?lY-09=joE#Bsd<}KCM1CED#q@;W# z(1Ky)bp%sp(8kd7-rXbHp`3EONQ2PrK$=RUFfh~M4SP9Jb-~JIrG#mkeC5j4tRQmv zZ-=1zOFl^%QNO}MLR&lA$~QX>+D#TTW50f7G+^bEm8(?@IdWT4BbKn^L?6 z>}dTMK{Au{O)8W=x?9v})|sT2z>qE(>8K3I_X=cQ%FNT)fbE0@Tx77KmIv?Q(E=54 zVqOiRKh%d&L@LSjI>j$~^)*JvxnOB)w2Lhmg_zea{N6jDayO~_(tDEto37Sj5+x83 zgHTY+7ZfYVFGYd1zJf!G#ZQK8~>Jl#b7s7$Y1 zWPzNWfL4jLyWB=Ito^bEQ4xFGfs$OrE9^p)G%zYk;Z?~i1czACz~sIL%>9R!Ux`Mstbm?p>&<*Auy|QwKEUS_sxJa@W17^koV#aJZoYquXFJX600kOBij!*hc-SyQ4J zwL00e7LC7IPZLycD0F3r`Vhv}RMtrJ@-}9<6uWA-nqp&+&IK;PNC0E!%w$ygsLAyJ zZzL|}GQl+)gPe;rwa%`yD*t4(c8Fx_ceN;wt^U)_pS=f`cH{N*A?W1MqRONl0SKaC ze!V$d9i3!OH$?lnD6A3C>FqtHv2i;4WA1gk-DFC;-=1aSSmD;5)9w5{Mw>n?VL|jJ zleIN4(}+kRnGZEQ9A#@#@X0#ktIix6XQ$)unJYeH`%_M z5(kENm3B84*q3rLxbkf(Q4|+2hTi!wBA4z$EJhyZm@4T8`f-Vg7zWn;>Ia|JC(B;% z2L)Zis{yJ4mQVqwIOJ=)eSx_DTR!{8Ws0o4lFU{ja*%yP;sfRi8DzB}zxjHW?+swj zGQ^$*F{Mnvo;=8J*G7LVXy-)WY6+~-7?{N7GDg0is>b!*okwt1SLd>u!8tRs3wwWJ zxzaZ(5qDo9>N$b)y|>q-)6>CHLhe9%Z=r_Qda@{zMt+m(*)H`0u?T_7pK*yrn0cTC zpM*7uIeq3rwX0oe?x&h?0|R8j$iDn4@Qpo`&t7rY-~KXZ{oBU$HW}0L_EJ?L^+@>o z6jl7@oQ&Q4`hw5pSo_>lIiIX}(VJ6ammr<4o2X^xZFEu?5f;6|WV9T(`VQ_gLGiUHH+xzr}Rb1jKn{C~Zl=?@1eE z^jQkPI`!|8Sb|804#ZoVB~OKoyDxTb(BG+!Or@;50!%pz!2@%6j}u;MCNU zZ$3?us-E6QCo*%^dF0rU!es>zoZ5t9^#mlspvdS700J_b*aH{ViTW+%pT z(>o~0u>ITiVzxu3ztw0nw@=zR^Kb_LXEL!FK|a$znAP7azl)B1_8)7#r=~M}m@QxF zGkznpyE;;s>4mPW?4aQ8fY;pO&1O1fHP`C>ctDd0?@ajq-R|LY#8Zj@BTbf$ubi}p zN1;a4LPB^an8D!S7s~H{K^8m?YueiI<{Cl8)qT? zFnp@A$?9m%(g%UV*!!NB7O~`Z0}2QZo36Bnz7DaNzX{HjZa+v7tS z^QFb@fn{)LP_^py%gQe2-+%D=oX?-hH8d`_87UGR(B7Rlu~xmLsaGOv%u(Dqr#2S$ zK7ZCD3u7}ahW2C%w{d)Yw{mQF+^yF)s8o;FWYJ#+3_quIeg1eZrje%S<;hnSRaEUo z32k>P*Y=qb9eIWHYv$oJ;RLb@&38 z3fza>kGMFvAbDYFO>#npGCD6@P+7;ikW-c)sq?hHlODr7?5|;twifQEzRJL`oYe2x z!}XGBnwUh`DQq``p*QB{=EPrg%Ww1Q#{};KC25B865b75=zng@{qQPuwRJm_KVN-+ zPA{kG2aCW;d8fnK_R#!w+VA&t{*%^BE(6k`rt8UdzT-_QZ9To*dq(upT18LExv=&R z<|kMrxPdyHgw#}2FsrmtgZoty3+lx*Cj&r*Tq5g>nIdE-^F=*sMB^0J+qdoGaCn=Z$(J9=X%I!=i`jy5S{Ex5Pf7AR&$ z;nK!hAQkT0lV2W8ioT}6-d$Y}%z}|{T3O4~Zt~&&@DZnali&M~P;zRj$?j$R;l~(c zasj#B34_LW_o6Y#JTDeSI=03uNi2p_0tU3Y*zE^z!ouH&y?<=6(s6e)T@yvl{el!* z$oU)(JHvH<3Y|Hn4*MW!zRuU}+&SNukdzBAGI6+f-(WW5>i4o*313xJ)X#0TZo$`A z@73BnMp0gdqiHv={3URKUqdjBAJgkRPU9h7H+9@sUb$!t?+&F_(qE++c_Un~id?tg zh(dvt!q8xM=D^smvF{f~KG3@=Sy=E2?3~SfFj+t^|V17#Re`g|AI-&K0#=Joq^{IKN}cNVjm4pY8B=Za!Q+ z%BCU&oTG|P5Z}7hA4hn|*LGK4!kFl+-uMU+UKVz&{N)D07{&zCzVT2mjfts!#Eds% zXWLh#73vR;rZ4GMs4oe_j^mT-a{O`-$oGuB@hXl-5F+epXh8*wPF76d<xHvT*&fi?9 zG`=G`-R6G8Gcf_hQ{r9XOFNsaF}SX-t{o8xk=QtIVTOl?sc%bORiY-A>jx~l*}gdX zK+|3GyJ+HjkD^WG!;+!4j!w4S4u;iYS?;A9qmK^Sc6v3LKr?;7Y|D+S*+mAK>891t z9~Uivd)@vW_zuKOug7N2byGJ8nN&%Sy=J$QA?I$Y>!q<_$t*&=P$l8pWUziqX zZ|-6r@LyD((yvnZwR$6V_V#L#Vy7iq`1x*EtvE!Dq+JENPZpmZogTk{LBPFlp|`jX?V=H(_fxo(GV4CzvF96u-Tk&JX}k%Ku;;chcNnLuZIayoL0 z`f0%_)m~OY+*D|>BP9?APSfWEDtoD1f~fXnac+DLFS=(UM5boN9g8@!OEfc(Wq-O& zwC{oFFa~6dX;4L^K`@O3D2XNr4y^Z>c5)setbvNG#_;^|9NpnDYzn$I4TC3=%r~nE z;my`Y*9J+ zZa+n+F^3z-H9x66JUJocf#6LomV_~+3!z*`KF`8-3=v6AUU(HsqS0t@|CbD518$<1ZH$+9`y}VWZ^Je19qXIj$bBu9 z)#K{Kxrww{rw9Eg!vztxt#>PY=Gz7(w?g#oehG{alf;%mNq)%JdDfK2>e`TuuYP8w zC%%cVat)T_*X54Sf0-&cP4n~UYM@Y`n1M!2->vR~1jigo9xWF6Y*2;dL9KXFIvy1? zY~sbY1{L;Ae|*J;+6?cZHiMru;N^zz>Yki-?sk)liBUfRcId%M9$rNOh)I0kEX4mK zppHrhO4!8#sl8u;=tTfw zEt;BeTEXj6^%K-?V2gOw-ZMeaqwsWW46Q*xsX>-j4<6I9T^D}K>ls+sdoauuXhI}8 zN$0Y2o7mfh47ACt+&XB*C3FDO&pITiCiWTFo_|?#Je69IQJY`HR)f;{CqR9T&)%yg z^oLAf6nzpY2FjK|jly44F$cy&m6-pFZbYEgOAmB~8_G}Ad37Fx`SHM;a@%;IN{RaA zCD=wv;}2cBM{LIH40LKx=N!ADr$@5OYE3Ee!445_g4!U%ix0e}3N3r_^22&$_UIPz z|D)`!JK!0i_!Rq*FRYkOqU4QYmQ!q`Qi2l!qJLUe)aoWlL{o_HW*bl1#>z%NV>{Q4RxZnu?k{Nli zA%r|*q8$maW-3A+M8s+eAbDqNLGslL9Ct|1hsVBabIhCX!oHX#StfK~4T*{`@Vzsw znG{j>Shd*NwidjbsbOn#3Z>|#m`YN9^u*--I<-ZWZ{b6UNw3J^+kG2Pe7ta&?DNAI z%c7SgNv&@(QBL9=7y0aL&_31Q=Bs4IE0!BS`fnaCUx=EMGMe;+^Xe@Q6sa%yFBlo> z(>VW8?vxkXIbSVkj5$i8U1V-*#UyNB@Ml-!5?#FVTNI&+%WwSa9mA#= zI7;90VE5W%YHG6*k~@fBa?$E6gKe{FaHM%X*NZ#W-Kd6ny|HBo;>cJc-I~}vWm!+v zZI3?MzA}uurf!_HD8K2$+7WiDt<_?G=R}%Zkc;Omu=e-tsRnT4NP$sGHg;-MP*&1_ z?R~SrXO!|5f*rLJQ{QR1gJN}uF8TM4e-IQo;~tgza91l+t&1a3(jE}n&T6NcAw`rm z(rnHrkhZ0!w~fTJqtt6;;mQT@NkUs#ZY80>L#bZel||M?veOQ1Q?FrNd`PDDiM?0* zoC>mQnlZM?qI{?qxUK!&v)oq= zU1F;j_j+u_JJew~c1FB*fg*^S2<&OD2e;!jDoIx;T!hu_*a%Z`&>cL7ZOo+1u>7Zx znL^~IBR0YHC|Q(M*?b`kXH~X78k;7;`66MR{y|4y#rsVeoEP`udpC#MuEED+fF2QQ z;(WBQ1~II`-EBnnl^!{n>3160qN04sR1RVj6O-d9GXi}58s=uDR({}!DH!|MJpx(L z^vqv3F3k<^UlZm>!%T~pY723CzCoJhEIk)N(&Ysk#pjB-q3DJB4HCu7<&oC+;ig?g z2$S@aRpBYJV4vjy>GJ9fX1v@R@Ixpl^A=PulZ#a6X7bb^H}5= z0Xzi{fI>#FjA8#Jxi`QCsJ<&dE{u^heE{qE#j-UkQs&*&jhHE$jRPsLIA_CIJL@9_ z#`L?#)UMY~mGAC)I9@K}yO`xnj!?%B9$>K@qXcVg0?2?l*Tb3zsKbL$=Sydnq980* z)MKWm{Ch?eruHf;^_Q=Y5DaC;};wAS`3PJ zc+S=I$fe_=2k9fWFnS9cYJ<(UWYNbFz*n*1tFbRQgL=aeG2qq+O_8Zie}KXw3fYZ| zKq8zUk_hnyr_}S&O?R0Iqka)!P1q1G4CvgGGIbfZ`oRM7`53uP5+BvTY+vPs2i`Tq zN`p)Kw+M4zhsDL)BMrW$XpX(LWWP$!kx;H`_W~i3O`okC>}mL%NcCrUs^Y~`SD)8& zCocolv_62?P$MMnHdY2h`~%#e%!A#g;75tfID6v(V1tIOpYs(pn%Z+ih+eS^>E zOrE2fONJAyBFec}Qq!$#RD7FQj8yfz!4VC1-1$O^yr!ib&b@0$)_c~OZIWH**L=o{0?lf{p z=mfMMwL2j>T}nh$Ot^6&(eWH%s8svpGoWufLjFgYYUxr4wUWD8pZ*W8tiNKX6QaYu zvPena-P0evhWdNFjF;?$Z(H%sC;^rKp@LZ>p`IQ9QW(Sast0mqT!{Pdbk!*TANrxS zR07)bC^$nNJb5xh&`(3v2$5rElpRuf2i3}L6HCuowYKJTCg!h_Dz`HuHNaYpQ=+T=^f$i9c!)<|;r!b_3hS4U+{;ng5PbM+kf$ z?ni$G_ej$aRbN?@k0%3RgSKqLTmFS`Cr1AN`XJywF4_w;G)Zs*;DdP~rKJ?ngP>nx zi;YI0=2~=RZt^IQdQB_1Ho%ws?eVIF!OBGtLV+xLEV*9j`!2DS@F8_lWs0K$qGff?^FQd^0BGEY2%dbcv!A5;E z_p?_MA|P2)tWm&dUtot?q4#@)(FkP70N=PY0|`zK$y&?B02gvW4NoPp`z9bww5#K2 z;rVjs&(91>b_~QmM?Dij~icA(X1p?!F1YH@RDPn=J z8DAX=RR6a6wnzX}>=?%n&(%DCZoG!?box*lYTOBpwZqO0&2iEIJa?&a%ce{q-aibJ z8Y}2GV)*!76ZAAl)xJQW2dTsD`rKC$@JnkC^;p@?z3kYRx)v58f{vf_3>K7W_zE$9 zK76Q;4P!UzitSC%2`T&_t%J-bk!avHAOA+s{DekaJ@#Jk`>K0nEZV464r|_i_#bQf zo5=RoCz^A{MmCzqC*FRD_R7j)<@oLt@NThp>M@sPFU{`mu75~KZ9-*vzD4uRhUP}2 zJHnXKj7Z(kLcJvuBpe*vK7$)*imA)M*x6^*PLljt}@JY6W&juv)zxl?X$aD zp5)Lz3^__B+DU>NQd&j!B)HgPx02Z$~LiE~O zy&r8O?M?tZO1cHKk2U2DLNpKpcDfld$@OpS$-0QnMVqItrZyAQd9!w4fRKo2;8V}? zzf zByEP=q(Y{Vg=V=2ezBGGXU$R^YkTX4ev6NJh9g!F9?JO@g|PV%Pz4FHiCV&I^^t74x?YnnShNx4>NY2EPzoRJ>lxXyrNG~fU zhEm(Ci=wh@?@dI+jr;fS)OB=RiC|G8LRo*p$vE?C&N@G?^{60>_Ysh2PAbh zs@x=(K4`n`)vI8YI%x-Q?QY(is6tcts!z%x=i(7_7bTCqxaO5_fz6*TZ4SANQk7hk z7pm3pmP^YS?QP+Gm!m*-r=UB7ft;<~eHluX5=2)i~Ggm7}LpjT6-$Qu}dZq9Ah#D7U0vkYC1eKt;2?47RY)`DY zV32Y?lvn#BvxoKe!;AkM<*Y^vLMb`AKTCZ*u#s!%Kn~pmpqL? zA>Gq7Pk@F!tvHks$D`rH`}B!C-)TuG|6`$Ak1`exRp3Y79D~gdR^ic??x401)lb}W zyDtPu(vanWoFqM;)Wr#_OIL(S-oVTebQdjV%`u@rKcNE3#)kjY*e&eGMZU7L(!pJI5%qyuAw?$m!@rIutLNgKHEELR(sMdeOomAhw6bWDJE zZGCsf+_GxHF8OhpkuO%Lpu=KdM|_zWf0X5e0=1$-{>MPJ1UD4>&30zuGEQ2u-K*m>jjfO~i z?J6e#6LqxTtH_JF0zZld?34krN>T~8>+?E_F?{>Q6Hr^bE**h=Y)M02O2f1Q28TjK z5^|?gW)#1A^vo>K++$H15+_XJf4GwraYjPTsydUL4egIVk8mbk`g{ z+*TE$R=Mr_MQ6hM#aJ0FKxG4H26}-C+#>eRWpoc@ ziMsRUW#&Nl7grsqb75zCYSB9!%T&u(aNE*Q@w|9-r|J;v6MdLN<2;g0_twq@oE_q; z0U$wXY{E+otkl&fAoq7B$e-Fh-S4-PjBGvJanS!ZuF+PAZn%>_KRj6{q*f^D6DjA5f@d8@;Ld zHE8nGsJ^Za$AB^Tb)t?59^G9WC~m8k*^5|8jqjU~9>ID4lZeDb9g*x^O_i*8^K4il#*j>+r`1pTmU!Jt@kS$%s1ft#rdmX;b4hzqNb;w zcxJ*CS46~vg2S8(jIG|%W*k~SU~rlMgR`-LsdM#Maqub1au>bTm&eNv(bbd2P~o9= zcg6goUKATG=y|gvQ84~w6o)i(vJ$XEXH}vNf~JW(Zym5?QMN?I^>#c;E8Pr3x^-Z@0e8+6Zo!k_+b6=0;419eE&Swi}?^4+Ey`gy%O@vXcPsuj%cBf z_Tyr^8b8l)a)*uWaSs72q=l2j^q{0LTkL2prg+VBUOsxwjSP9}(!$iQhY?{ zB2R&;_Ji4rPQipnH(RktK;&f63~*@EqGu=QP~NAFTK+vBigI$1A?cCYqxcQs98H4e zPEK-$TepZnFZ@_5L=Ykl>p{Q!%=DIiVPIjGdlgFs6G>krq#UQq=kTP<)F|#?aaxEE zP%U>z;k)K!{l}W9p=00VE;vL0{a{_Srxl!crxA2O@9}i8m#_v9CcDMq^Ef_duA`4R zZ^|*N4u4}xNNCo_Ig8c;coEIVZ)$dFMzz>7!pRE2Qcrpo>Kl4VW}3oY>zH`jNLXvS zWnl@^4X~~zto8^}#0xkyZpXa0Ir&W*>}>p*f)OK)aBYIPn3IE}F}<6(>rYYB{=vd6 zKDTZ8-P_}a)vOIwmE{GAER~`>=@YtbI$XG}dn+#UD*okf($Zx&=ZI>baB}Ff1zY8~ zL4$&rHzPzFRmXws#T?QxMhQ#<{p3Fh6>U6rlo~Wc?}SfOR2{E40WYsQg6*(fzqEkh z>J`LrRB35w!hZm!eDxet9*mLvwBRmh2w00z2D!%}sA}v+REy4{0H zv~ser9A?9^nX1a2A`^L8P#UdOsit>nf}U#@)dSviR3TJ(HXIYn-1JZyn-GV}Z@N)d z_w@5niQ6p__ai)skNvBS8>NDt6Yv2~>O9$~gcqYpw<2>@UyHxl(-Z5G=d$RfX^p(o zuqNu@KlYJ~Eb;}^I}dm9o4!LG8&FY&mVI&+)Gr6tX!k8XnX4#Gp z7WCS5_;5wEtH6oFSqh%vw=lbw(9k!xDh^Ln)H9ftmnPun!^OTzx(t=v07=<}sr`J0ur-wm^roW%k8mcfFsqytX&^RadJGJF+};-6L7OFGM!NtZDHOv5RY^yM$nm3A5n*XS`Fs_-M(N>hI@uGZD)Z^rU$P1%0B=D=x10*E5j6orjt{<5s*r7(9JL z=JS(IlytQM0*{eDEPyzbeI4h687g+7LJj6|PPN*Dw5_eZE61ID`}a8J$ajmnUD}V~ zpL;&tJCO<>&!)O^C9tz9n#m(wb3NjslWT(@pTjXzf7X#?Tjsr!ro|EHS2ef-ew=|s zG;?LE!|acI|Dd2o1i@mn$_yZLzsV%MuBDyn%AzpGn)yQ8$`mi9G-5%IEmxa`Wfd8s zvEX%GwrfrzkY(yB1Y}6;(XPg@dXgfh=Ld@^+^4df(XpuAQ~A(dgJ<(r$ny5yJDP`( zAO@7l@S12Q6cq!dyx<8`jv~>FcGF3I1NuL$D4!H;d&@6>y zl_a3O2L6q4TL*_cb9Ag8_v8@$i&2Qcw0j|71wI=^M^83?eQ!O#jg7>dBOfapgw{tx z?%Qv>J2@Zv$5*yS-D&7wrA|B|5CnWa1>VK$4b2?7?Ndj%Xv#b8J%*ByjXOrNz*W*@ z4w32IY?n~jDjFqCelGd*UGmLa2s_%?*-{n;7;kg0IKMqzJle_7A`tD>c16O0o6 zs9I&qA(tp{+7Q$Jt7DL;zB$R}i&1!JnT(7~%{&DB9%IZlH0v{A5b+=GVBg;|%BHUz zR8Q+M7|5|uJ#GEW7>XIbHZa$&{bJ%f8VwCiV*$aH;WwpQp0952blWukl0KZ*ERuP- zy$r7O4vYKRSh>%hh!f+{i>YRVlUDR3tM#kTzG`Z{F2Kg#KoER!)z-TKH*V8TjAR;n z_GYQ6^lq}>q!MjEB%&wp6eaQ-9v+(6;jv1RU0A#JsDJsTYMv7h0p0MOx}F91Waov2 zm;uuxD#MzL@1ftW09~+wE_M?)G|0_*2{Q0Hf8VfKo^_AYamD_*_cU4j-u@?0bKz!S zeM(cO#e_S4cK;I#fE4@>AU(%D!Q{+<{8CB=IZ88Tf@H62=v;JpGFtDd@&Oog@6g8` z03CLyqQry4LjjJFP0fuWyc8eCzp7VA*g$m8NxEgX2eAo6|g>9j6^2 zq-Y>G=oC)W%1ChvxZJgjukX0LB?1VN&%3=JW{uabkgm_>6}+*ZPvnvtRy)c97T}wk zn>P|n6lzw9J}j%=*834#zzkq|H@dm&;66qm>3(W;Nt%y%L>fWD&WHU`yGG3Y5+FMJw82RxD z#LzGIgc#IhBsI2ug(PwYQtmub9xfmLRIAZ9HER;)FRs8>eTuV<-6>jmZO?H5sYS6M z&--Q4kJK&@5J6m7Z7GmZ9Vr?{$Q8(CBgu1_G(+{YziSrlK*^1EY&bSE)K6lMZVEULe%aU;$g?f1=@Ye_s!PrM@xXmIlgQT7 z7k4Up0TtY|gtEJ_f(vbgFM}p#_ z&;o6s8v$&3t_!0B35t>C$?TVLI~mZGlsPQOx^+lH6CeJTzS4&~0%@3J-s)>vLcWi1 zq+CuG#f(FgvG8atPGxW?~XZz0T2v~*r@6V6Me2l&TIIHf>0+mlsWW;|onUxgI zNUhzTXF7++099M&;CcT5%2)9xLb(&VM_C4-PJxs(v`(IBa)aaaqvdWOAcfA?h5L6E z6S;rOueQbc6A!PXi&iizURkv97&DZh32hl`q>=plDd3S@gG!Bh^6}q*6DkX#R%M{G za^H7lO5f6d>nCmhWH?%z5&W`E4P^E=Qei0fh*gh9iU3R#{e(aF6m1-KNF@?V-j@gQ zBlLlX^m`_{Tk?)*0$$?Zsx&;o%LA<`N63<`^HH@E%?UotJ+ zVAUvs2PloG83ojIM88kwx-UCk@fQ8v->eO6Z``nGNuX4;o>WD1#ykK+>NDDkEZ&?$}QNn=Q?$U*~ zR9`y(I|it!sRikBGceG?4=vI5^d0ZObC56>`CI<%ib>kC+q)09RHkMP!(7SrZR|Vv z7pEZ`E*JI%`0I%Su=e`4Haw)L&xsOs9!A^;?x{cMa>GM>QcqAEbGH8`uu_MTl85Y? z?WHfF$*4G)Uk})Uan242TxwkVgHrysw#U{-vuHH29#m2cXWyQ!Z6g=rFTf0l>kH5q zWZ9yL0NZ+jboKHab!7A*P>sq;B?&_qpblDFBKBTqIQN@S4qc^&yYqjmTP^YI`0Vbv zv{Wym5}WNSp~r5(ZCixf-e*LU1j@Iu5rOgr0vG*hKafCvIWI3K-3Dp+{X1{48*TUe@<6x=`*ceJQ=GO9$3LF`FKL2F2WD@J zy@)}wxR6d!XqWlAE_RaKq~*KlJ4DF z>hxgUB_S-==>O)Cb2^$o6?51reuc+tPsMZgK zzilB9o(U;rN{*w#L>Q7TMA_l<#NWtN2YJsJVL6+wfOxQH30hpv70)p_D?)6fgufg# z5@(3YMXN%6gjpVlIh?vu$x3#zH85#K%7IHyhy&JkVKoD{60hj2n zZk_iypGt!fh(iB2BY>xxIB^&H1xDY^zXlV8Oq+1GA%;N(-Ot@DNcJ4-#$xh8S#h)`!=@WDHv*a-F?iLz|^Ze!hPMc=P}8 zCJZbLa1D%Z{_1h2QI%`|Pq*T5`X=HR{oGg2V>9vj2uRfJK>6h5yV`E_(jY1%KY%VQ zQAqmAyAH{=?$5hjgJ0knU>fA3;1n-+-uIOv1kpVA<2w;=3_*)7 zvv}oi47SiQnjnBBTRyULXG7s8B37Ady^>Q{*; zdr|^;fsZ7`u zMW{&R&QIWXUA$_~n-?J@avs4-qZGJGZpc+iBag#|t3(TH4KH!C^!kF&aprRoylOwg{d&E^`;mClmwQdD6d|7Lrb-*% zbWI?`iWm_#G0kzLi@Sys7PCb_2_#Hzu$He?2csek6#*I~-(G+0?Fq=`9Uat7TZCcG z;7cA}kA31!*td7vfnn> zb;cXDFSwX3Pcek`fTnD1M<7QePN4mk-{17u5tc7<rGP@O!SUvC0} z=HnNrZEhy)Xbgn+8R6Y3BhiHP8VBK*0<*WKg^t#*bBpf;lHY+&CeG_A*Qe&&u_Q!G z&LrCVmHYo}?|PSKVz#ld0S<3c`Pucg@zRj(?d^pednoHJ?zjMs#1_+xKoR1vjgWT* zz~@3B-p)@Cm1Sju2!TWK#1DRz6LDgi&p&@BL(c@=fP=VR-o#tyqazZ)No|n?GxBfo zkM}F^4RHKx3JX0i_~Fr@K%3%VxfQu`u9|nrAcdY4Xk-Tq!Ow<4Jdo^?Qg?~PqUem{ z53msAnVJv5;^_GG>oUH!*()B4aj(ow78t()?~rCuxve*{8-j&h=^u=D%bM$+&j8iT z&VXReNGs;ovS-k?8We5^Qw^*S+aB6@0-oqYt~~at&bmJiDKmY0_(f`JY6);*%^iilqU)lQL5Ot4jJwUZw*% zZ@y;_2VGKi3CoCyxs{_+?H8iaKQ-;U#ZJYUSn9XBLK_|vgW{uoCh5Ik zvZW0&ZbKbJQ)K8NWF*vvvV2fCA^KyU7`HZpf4RU-$tW()3I(T|dq*V%qQp zRQ)ugFiZC?=-h=DCK+@lOyt}4T^0&a-K#w>aRK zgk=&&BmBCzmxRlF5M3$b8F^8*TKRqS4#hk{^FJfk*dr2Xsp)*migY9)ZJu{lqur*% zE{pyd`f?})_Srr#&-wXa?gh~1L@+CnaVH8pUbI{25Yzuv;8^yc^PR}qC`MeAZ&tq< z(6gu@D+uU`40Nh4H_$Wi{w<8E@ZFd2vL)lpdnj(R_T9ZdV{IT9k+3p`#G zM^<$Km6Z#tz4;$0To=1UI6A_OynMdAhmrB7_^fJG7cdD|BzH`?iD-lXLNeYs4TCV& zPqm2N?f#~HJ)lwCyi2Q*XS#FOsl+u&r~2L>>%E3ozM?bGD|am~OJz@~VsDM{+NBed zdBSPRN|$Z%kl#fctt~hB+@4nt&i&R&WfgHdvja3H#o74CG}Fw$h2+ooW%F<^5eObqzgAgt;G6|Xd=O$LxXV1?yc0tL4zY8(xX&F;!W zn}+qTPn#35RE^s;uZMQKFRywJNpD6nso}W0V0Uk%;NDT%TJ{s~ckq82`A85L{!|hLIrOEk~~2S6m_wwA4CE zvP-i>Ug=`J*_+sT{OjF))%bgNLT^SowEoXfdBPsmfbo3t0gwsWjX@l;du^GFnQilIv%%7+!31 zxczg*t8cs(r6;Ye5ugwI+XZ{y1_I+Ng~wM$(eSCc8aGLUtWT%af<4SC_)LpEtsRq7 zkKNt0L1DAQn3KUgn`|sqOqjDlM<;sxr{2>>c>1@_x(2r-=@Qel<9KsSK^oMI9YK*? zQZM$PeVXdd_-e2jCAr`|=glVx0fGv}9FF67Hn}ikAm3oX!F=rSqf_G2Oee3-6L^y! zZj+o7Zi>6C!C+oDQYedL)$mFmc{?b@bS0p#x?pP= z$o~Iqe(D=m>ra7ZAIz+jlh(=B&MMM%{FAjexeBR|ys#dgQRW@)M>ctioRN`>cg!69 z)G^E@hLQ00PBV`S%?5}vmD%b6zrc=6OWGh5`!ku)m6B={maU$BbDa8&=)1+lYc$MV z-L4uUCWX(}s}i;;*TyOtpuBt*nJZ5@Gww_nb=%iqICx!ha;Wej?xPrihcrh=e7X_i zl!AtvtPM>f4J-UTQwu5=0lnpK;OV$Jekvh5GS&4O*1+=%HamnO@iGDdv@01 z(^vQ%oW2BMnWV(0BNLVw6Wx4q$vgCFU#a;Ag|%McWp|<&E?#DRH7E?N>GECd=pWeL z@Y|&_aINU?mi#(J*`KXH^LOm*ICN%4>lgBPqgN9v&8Lkxx05;$WS&rWX4^jOUDP7D zEkaOIG9^$~tR+2smhVX3rJ(6fU6WYCa*4sOyj<|ZuW7{>Hc{7hs$PDQ+8}d``riBE ztC-EeHR1=@ST|c z=z!IzI8QG}BNH^2+MT5keTS#&e4e|9o@m;Y)r}pM);X@Sr=rf73wPqg1E82xFYgWm-=MZio~>R~A6vw7$#x$7rkoQ7GKvP*$5lKk?)hM`+}Opt zy4=CKv;t%(DEFdy445r7t4%3t)eEH99>pBrluHouizyI|`ef2f@TApiXK;hTZ}M4I zih$Xn+h{4V-vLaIU`k^3!=v=J82PThd`AQNd-eJDdc#t`NCaGt-u^}L;f}_=P+ihb zIjRdg-9%aXu@m>u-WVKYH1ZMKqF#%xbXmnAV>?jK$vm{C<~6!IT(tb!S9V^1Z>_p3 zt#j+eaC!`$CK&qXgSnRRe$iYTq{rK>Vf#i~v5cp^A9e{ptcHXHOk?$%e;6GdgTj4g zPC0=qG@Bsfm3Y2?|K@A*^(&5*NjGn2bwU_T?e9gyFyiIgQ@?dSJ$Ul)&LQy@9+>S^ z^yf9E7QVe7o*fhuL6aa7j=7+0e6EfzViUQYv-qTm6Zu<`7s(}*OsDzqn74YWf#F>6 zhb8pj%10?sq0e_%{6REBMEa5rlW_GteImt_TD3L9&Xv_Cj!oP^P?onP9A3eaG*FEzd zgV&jeJ8XI0vFSEbkhfm2?56+zmMZYeoPGr<=wKuO{qgNabqx&y8H4bOE z#1pVIP-P9|Pz*CkegAIJg96A16w3*M#*+N^b7$Y3hJ;4ToW)PirfZvLZ&`iTtCWi7 z+{2|zcW^EfEPJ^e(*AK^Y9U(#PZYaz#~nr3WxKJEGI4V8En{x2shJrgUFx5EGP}G) zyu7>(8)39PV|vavfmK_mn_;xn)CaChQ^O1DQnv<%>9~&~-uQA`Tdz^Relt377xM^- z>)u|F&LbmYB?R<@JQPSM^8^GLL_`vxVF<2{?lqK9U2Y<-C*t1b=nX;m(`tzv1x)Q1 zv-BH`I{#-`m-4AX#6IX?{9yuOw zwi52I9koB`UY?v?c{MJaD88(W6ynv7HKC^G_Qh8I4B%&Oo84SA0&W*{IpmCsxL@9KWJv z<;kuo_q&BVtO_`ZkK?hS9WywQod$a57k`yfmx+v^eXP<76ZM#27N4BFYsg`9%wqlW zWxeZqb*Lbb<8xO`!YkgOr1j>0->{v{0mR_xUeghiMyk-b%N{BC5eEUHU4$ng&I*}J zYqhst^*wvM>x)XlPL3i+bo?ZR`8%OhJUMgyua?W@X>{U)Kg;J8rM|z14={l4GJ3|! zgA5`KxsZmX{P@-o5srLKFoYkm3i*T2aAxlTkkw2~_fiXbgpY5d=u`QKTTiv+e!8AC zajy(*krU93hb>0G=*=Fu^z>$BXmW!@mUaQ&;WJ?>Uh|vMacld_j&9|KFcN86A=~9~ zFfBKcgTwCNe!IYEC9xlH#>$e{Frtm%>{>f1j_Utkj^@k(!8%C^T@6 z1!ilZ*v_t09B>jPUb-Vb@X1H&vt&^J{?Y;)7B)Z1lP6EM*SI0{h_tm|`bjK3_u;ly z;!*$stw5{0!|D(U$K68D32)o%0pFmix`ay4pRJ$Nwv@pkB(WT=@PilgT;=B{YX4X) z)sxD$(RxR;tpfexG%u|R{J;?t*k#>Q`+(lh4Gy=nJb@@lLuvgjewN?gUp#FU0iMSWuek$eH0s2&t5E>^*hlyA_+DYr{Ujw5~WV1)WW2qfvAWe8uc{`IdV<0`CsuJ@m zQy$**^vvY!B9^-hj>n-%5Y4TRk&(PhIf*8jcnj&eMBz@r)xUro*s{;?2;WeWX_RgHa0=e#^_w#_(>*1g2Z=9-id=p3 z7QkxcLzqaV@u3;PlOZ!>`$t1cpo%VrVgSVw?(a-t(%#b$)F1j+13nQM+N!5lY(3qJ z%|EtU{}?^8Q=|uNEg>QAd!dws1YhVC(crw1;?t-1;H5`^fHI_@q9yj>G?u$IyZy6y zm-v(6m;k=QS4BfU!E%_=MX+!?t}$Rn`jnWfKSK?(FQe~udHEYWr(Dfa@Z9!blJRQ3 z!QuYlC%uy<+sDd8v*Xot0f`=`YofSDu1=9+l`i;~XjGN`D)m47)+s-281oklqwQ=T zL)-byB{uYJZq6e=pTpLErLvqTEQF^hKVLGXMMJs-D%Ntk5dHXQVl)~2UK6lwBur#;S!fcJ!Z zJ=d}gt^HK;Rlr~2tAJMMQGi!Z$N0Sj-r~0j(@vC2lrW$6XMPBb2fn~M%QP6*FGdP4 zK@WUe&cDidC3sMEUOglv-B9l8wl5GeIU=P3U3BF7U82mRn$n-9hf2m@ zP3y#tfZ1TueFp!OnwzZ!7URqU@WU)2x`Y^`4H<9U`Q|;px%&`G{;jZxI>GkTu`1ogtQu9psEw77O-A$H5PPI+I($Hsq!I53CHqY zPD#}f&;6>(;Mv7E<3IBE4u+ujFNp%QI2oG26Qqe1aZTs%prFF4@WLmrB8o0&nZMGm z*Ba(N*^ytWenycaG86#k7`U`|Zk^3h*f6sq`y-h~e$n-nMfem|lA4=m>zNpM2UU+X z4AJG>r%T-0L~F{yKo0fdyiNXhn>N4{S|HxZ$a2Z!26Ft>Xx*`r;sCobP3sR_`I?(3 zst9??2C0Rz?^)Lb^C^!N*&Wr3}N z&{4x5GJsUiQNAG8PT&?HWDVr6ssOJ_FWX=uZlDz8hE44iuY{vEBJc;*1LsYz8eQns zl#rGL=Fi(gb~6!7oEK^;d6zBtkX9jN#J}s(q_9V;NqmCfrL%4lci(;4ejqC|lSQ0b zO#|NiUmd_LZE|ol90cQ%_bQJcj`mmiq-`bH4>?-;pv($HBypK_UP=KfY$HhDyOvlh zN@j=;zIwGX-Hq0=sof1%K6s(zyEVHpr6QfJ6*`PSm}zNdqvEvyB3Xzeor08%6Cq@r zhRV14DCy+EEnTF)v1wi(VGqi+ZA7)6q`G zOXxQ4vwFJ@}J0548ET({5d>O4P zf(V!RiJ_deb9tHo@&AHvC(U{Fm78K09_ffDH>6Fsg$>x)jBtNB+Vn#He+s~xEa)Go+BYD|x6NgG02OH>I6j0n22z}n3>H_9tEWHQAN!6^Yg_5+qpHREzUAN;%<_Fy)^}s2#5I@siWZipW6YrWbFom-rH!b!H~To*y1ThqZr_F=qfi@Bkl7GntM+*rI*l9CqoK zM;>@r0LR5Yf5A#8l8>1eK;p1W9`2nWO{uz)7a5+_jLxSMUgGQyu8x|N&ItVJ7(yB!N zoYP=1F~AsZuVc)^Z6FS^v<1q>bbQBP8TlEc(KO+uLFRw_{SWfWfQp-ekfk!~IHsvp z&x&Gvf&h)4iQ$6(bfb$cd`I_|Jzv?#=f>niCWR(aqO>o0o;Fz9`7R;aC@VZ5@kos^ zm2BYvF{y|JB>hi?bnXC1er+{aV4hsipeiE~p4K`lf1eydauCv zQ?NwoS4UFE3C%YTJPF6wAT~yeVfMn_3l@&Wh)w}YZ7Em2}>^SUkV>i z=3+1mV#cPP(vekzJ8^ z(QQNd9E+>wG*{5G2#bTaSv3z&ZpvAdc(pw`)t>JJk5HEiIi=0K8gfb@FhLOmX#b(X zygvhh)jyO8Ipm;DM+FX2m=S3i3PJO|nD;R@ ztcY(u`$hnbx%OxM75WM7rp5`YYmb|El(!rstf5v=6AjuS+o09AplO*R?#*pI-BbCC`~%$wOwoWK`71tX~0>&eCw} zRfGpdfjfMH3ZobNlM#(C!v%Blt3%&@{AjC7pll{`IK0C8tylHoE=(1H!24p>yy7h+ z#+H8%vc|3S-W@LOzbGg+4hzk<2e^{H75k}}5*`~H*zg)XRJXk;8X=a`U&Nw8hqZ>w zhrURKZKijLT*&7owt2B(A~EJaw*xE(I0fvIj79XXn+V*I!tJz#6aCZmvBjZ4Ah+EQ zookM<{B@Ys)M$9Nu5*IkD2*+-gNWq}Ryx1#%bk}}s~!=PIup2`q}Qfq4{;xqL_MeB zy8<}`KtCXBTneQ_LLYf7IGAtEj5H_*91<|aW++qp95_sX1kJpU`z69CK!;i1;76=` zBW0fH9HaLMh{}iSN%LMs&l)eMi>5ORy^6P4YhLCn-=X^TPBEgP^Eu~9 zAM`%k?&UnFMSER@l}+;&SH5K+%qXj9S&uUgcxM|pncRTxmo@YO&~%%*df&r=$cbL+ zX#K3XSMw6?P=%yl)R3m8nXpEQI;-#v?#0_7eJc!3eH$|EK`kOE1k$#Dy=ukxIWN$l z0K8RGaF+Fc*!a%^Tu0ceTW;LcW&tV$YD}_okwd97B=|7tVScIj<_M+g_0);$)81pK@=g1ExWHfXg^(NN2 zsQ}atceVa#B5Df)n_M|euX+jXRG(zK8DQ=@gKEK79zd6!VdeP%ZBa3L$q+HFF9@a{ zJ<*!fDndX0{o(#-Had-l`<1s*Ur1F^ctDi@0&gqDoxo{6AT!6Xm2|1$IN1n>NWF1L zrwACPzW!TCh4QwC93TX|e2XyZ zLZzo?n}YmtWxHh5hMs`6tp&1?YSQrh197h07a2(lGDiZSC#Bj(y&fj{3qVT@u^DM5 zU1lV4_%8>w=p0deFCxtcpZ1ZgOnu7gr;`1V$BQ%DtS})_%v&G9sIA=4MT3pwgWuGr z+dX6m*JltBZCC8U3pf-?@Rpa%%#VWVMK63d=a^B+$Vxm;m=TL8N7C^a=TiAde)-Vm(T580*BjleXI%Z4qH(p#x)Q>$~h@!D^oAdkHJ0|w-9 z#;J_P$wo5y!bsOoFuCGEYg2p zBbTULa4BOb`L*+Q2||Cg{SZ@EXO_-0Vrca0OWzZ`0>FeMu>^Em990Y2tT~PbtJC1^ zN06zSUy5!ldK!uGr`PrIlv)QZ_EbPgg)j*=(L?|0kFS%DU_KF3`AOqk(-zp_RFHI2 zb*ltmZju**uwOpU&i{Lu@InA;4paV92Hi;>2Fb?B;;+A4>k{%f#;O|ohFvUfCF6>e zN0x;zSd2JGkbQfuYXYy#c}m~BF}C;7X68_-G%-7y4W{D1w$yTM>Efd2KESDfaFRoz2V9`@X3yC5h#gvNh@bC-97*oqQ%UUte5CoeP^LHW+}{cc-PadGp{_sw}Q3A>amV^C9btgFi1*#39d|6%Vf z1G4J2wP9%_M3j^U=@uy|5e0*i?naRg0qK%1K?wl?K|ujY>FyE{k?u}O>H5avIeS0* z#D35F{eS=6%DUIR)|_LGIp!GGxGr+YT(y1cc;g}N!G#XmHO=ofc*;}Cyzk}@7h7;c zUB4ss7pN%_Iy2;Z&OA)I<)R~xa{Qrdpqu8itC*2dIi`h^G`O0r-Bx1+2G*uWo9SKL zdn7j(c%zrkZo!g>>%{Kr=itcNwh!0U3)ajt2@)F`hObO9uNgx87ZsJT-Kp@Q$Brj61FRWdkQ!joSbw?-JWtaiUj8)yXjcBrgKBI$a=8MY)L zM@lYvd8oS-Qi>FrZK?}-PO~H>>HhelZ6n%OfD2%apx<66Oj4sg3{0tagQFp5e`)vk z7^Bn(FsK_M6=B;`O3$+y^(MxqG?`LyvtRkFC8t|x&^F5=pb9G`g9(E%2^;c@%}TL*`@lv>**(euRjQAkgjes_xhOGpBr-o^OG z@o}pls&e>%@LPE%#J`BL3naj9%cHD#^S?9oD=g%=?8r#cI-py3(n+8vuQaVzpnQ3m zASdKk)6JyAH5{%pPpRN8lSJ@h;ox=q_A%<#klh}bbc!_wT@V7p8E~Tfo+$Z8Syz8N zuRCm6S${8UE9@@3eO0I2{V_=?K!nI{J->P9j=a%!=}J`m<;c>B!^=A>{4H0-4b^cF8%VC$zw?Z{`EUZ5(+rE@T71lFs$0b(m%b;WQ-cM#~xR@Qf^`a0 zkG}?u+?Ytay+*nD#Y;3p+%j<$6=jAke_ZTmmp&Ev&DM&Y{3=@Pspjst(eR%*+rJl( zA_-#SqDzitm2Q*ar5Uu#p4YWX%tN7yU%1V`+e(8Vn#I5scCie>1#%>Wf))@UJ9WH> z_&%Ta;qR#K*o*;qC?v;nM!E~C=48j^9uzfan-s#)&wCnF5n$wEfggS!H)tIl?$LyG z5N8=QclZl=ZHl&pxe|6Ilr%LxV6YWgEg4lu&xe0D4DwzrFr2w|Ts|WhMkat+Y|+Yiu7rhA26Z2D9O3{1*a&m$UG%ocL(Qy|y zHZDEUK>tTI(zll`kWYuuI{iM335uw&FcSoNIxz5ri zFa6Y&C#ZVXt0nBaGu2t~AMBGt8ctj;pj4cJHl_zsA%j z3018i<5}=+!RIeZc{#pt*jhQ0$y-)vtoEXZI%#TZT!1--}xC4(hmKiw05M(8ABR53!5lXM$x1Xm&zG4OxlTFiD1SJUN=c3h|d z(Gu0WOg&gDlN)~P={)`D;n|MH#MvZ%qYUM{q}kz5oZ2PLVC%Nae=f=ghZt_1LUD`-k)Zkk^7Zr$K!$@qw& zm-y-DG6sxq(AL3Dn>psVw{aJ?33+a@Fe$hbLsxXetErRJNAd!z+SyLJRl8dw_AeJetIV`+S+#>A z#dBA@q2Y{tNeh~PWs{F@ap?#J4mo5PPU1HH$dJSO7QaI-|4wvQqwdyrr%M?PaWF>c ze5$OXFdn(+6$-&GQN0Htte!O1HZ_|l9q4h*7B=#}wJ8!UVaY6wMg`QXu;Dj)LfMTm z5~K>WD8*ebuh;DGeow-$|M_tnL*c3?>Fx7RDoib zUH?7(>E0oh(DN4(07^P)qQQLhh`l_SLniNuLX&qN$6ESdWk#P-R5f-A_BYS;uHgr$ zNIf*dk+-S|v#7N0B+Za5&>YchY^lFiA`jr*l9!2*e5i?s4V4wlIlr6M`&vt9?~7gH zmI&Q=<(6Sa-HoJ^X(wQl*_LY|K=v;dq~YSu)vXKXClJWk@=ur?L(~qRh`qBz*@sKl z!*X@qraNLD4VKyQBSKQ&a%6*g(j1;}-PO=w8FyLdm*Rbwq&thi^0oxT+7_1JD=&d+dB26m2VK>MOze~9xMnP?{AZQSab|<@H%Xlg%HbN zi-AF7eY~1sAPE1&ZCT&Wczr0HCYgrkZp2+N=XoMMx8GEwRW4|uvF_pVi(dT}6oO6w zdHMbFUg(0sK=M0#bc4{Zu3N8V!F~sP;@h}uCeH461^t+ z&kR?dFN-N{Ui{ztx`;RJHs^ntltAGfQpC6#Bcfy|3CXdsR{Bk!dX(lwf0)SP4^|9M z_D_jmi`f&reRftg`6fBqXE6V|$uiAxVp8=$%R_l~4)K&;rYFVuPWis?LOFAOb8fDW z=^iINrXR%r$X3>lrY$m9*D{W?#JY69CdK#Hhv&50DfxTfhs<b=z&~NSn!hM-Rcv-@ETEci z%lCI)M*BZ_FTY;_%{U-MloXy;sVW}(V8<6mA%Y>|d=d=h{pj~k^O8LPCT+AjS}c{T z%2vV_lRXtaFwr{XKzHN9&K^NietkOZk0cOmMzpy(&v`nPIBDb?FhZ_z^N`~8<;$0Y z2pL-F7#Oa0s&h5;vB%9eU;i#^4zJb@Z30(*zr`ih-%A9t#{APwm zWG)!A zrNrs*0dp*;e$bykH}?V4_-Sfp_SPqMHIC>ht36PoI~md&thQkWZbn}~kqe0k8-I&` z@JXfMhFY#x2q@X4xrS@m-!kkeIoN(WwN{NC#Om05zxTU>Sm=c38Tm?o-b+x}M5xDV zYCaY`A97C;b=29FM6cwQfAJuoXnVfahVa51l;r=8H|*ajypJ#BWe&#pgeBm0%Zqp zR04GSM0$<288bhe-)N6_h6?Q~##aSAEZL&d{%m;_mK1pVWr|rQ-L2Cq+o0v+#NFd+ zx=otxC(ebx^qCwoDtBj_%@&Plk|IH?1=6!uCW-3jRX7n}qZo>Sd>%0l5kQ(X30>%3 zzOZVj)yLawi12EnfCa0JX9YyblkZQ2>}ONyW*^-TYMjfCn^UPRHt%nFq&yBBBG+mT zoW!Dc+Rc8YvSz7gvkr)!oVMCTCA|sih{lXG??Ak+}PcUnld%hU}00w@%{d;66o<7<7;g%MPIo(wKy9l$&H6Vu}sX| zmVaG*5z7&rtLsY3crWtaquQ1sK05nau6LCkKX$faLP3j+boKY|o@zImva&LRao-`K z=e8+T*P2*{>iwiUcC?#4L8--T_$x8!EC#NZST?lCWzQviRb^9ej_-N>DmFQw*2zl7 z-I4>q;l34)6m5^oLP@+B&%r%p0+&9XJTx9ytF=8jR8`4Vlahbue%$n!L+=*BMd7fz zKkQT&s`VnFsDqg^bX*1Hr$);8 zh)4Kj6|8j6Abu99Z~vzv1_Z$WTJ*c!i?R~eJD;Z1oMk^e@#)i%{qksFV!X9;UxI0w z-APswTd8hh1lbR-e9AG_203nm8AOH|;B8 zfcRxPk&|NXYrT<5HW>>0?#kG<@c!XG9WO76wKa8_=M0;Gr52}@@+Pg?s(Q^G#`5pa z<~mg8D##+_92Ypx?>i>FxH6xx=jPw-ENi_tdi z2r}L}I_%v>*k?a0lMW6IGcfQ;1>}o39s5A?K+00~>!?CbV^5(A1^MtTk;*owkr5Gq zO;!(XH8R~Dy%={CEIt5PxNgxgs(MM&RE3_#p85w7=%-VeHny7$F6AxDuLXGR1vwee zKC`-}U+UuOpt+_@orsdnTKtcNw?&jc@6G7R9mle8DaD-Lz=MY-Ki=ayKOHgTweBCe z>Gmg8_3n`WXwIbN6vltMlyoO$fC-A2))1gB=W@G2F_{JFy}z;2pa!$Gj3i>$4hx-! zk>fa9#zApOaaYMQNZ!1EF9=KKI-0DA8x{a=sn^z$?VpqC1M!?Ou5yPPvL96-7^ zfq-KTltdNl?!(Q)(c2r5qeELD939*Z?9_4M5&QxlOLRh_8O&l%jx1zav&$1P)+hiy z93Me*URE)H_rl1S!8$KJLG9hbXKTMrnGJ?Ws(!7meY}ra~ z4x7b){K!~XSUBr7dI}Yi81-}3#eXq1GelOlWTkr?$)!PCaq){$7d04&5qQ>OU9Vj| z5BSihu3WSn6`Gw$S)Db&h?G(70m86s|HK%cCk8=jKA;-{NDNfl!II=Y4=TbP?!gyT zRb-tIEG`8+{u5<3fj6iw@Vp7kOlvACs2@+z-Vdx?zDm{1kBncRY0J!cLIUZSb+@y7 zdj5LgL$u89P{W9&-mZa{nokU)L9!l-W^hY?rdbr0R2KLY!WOuD?doMRJdpTU{B$2- zbt&FMtD}au3W2(5R$F2IV=xt@^Es=mY!w7s{t|lP#HRo>!zV=ODI~Nr_Zb45VCu|G zk_X|ba*`x^4dDT{^O7mhA7ne3cO}CUb|59wuv_`WaKjSB_Oi!UIpKzQ@EjeV(6l8{ zj1kyN?Da@xS`>9s^$(^&{2Gore*6@YFo_j8|HH)n&u@mD_Ys6tP5VB0R-`{Htuy_! z1t~LjLifMdQ`{fGUvpj*f=h`I$O|3HfgLh#2k_Df3Os9oQc$eJM(obJP3qk4GLg~s zwR6T{L4Y9*xF2@s;fBCguVV^Ol(UNlXNbdsL7x4??MbfM|T$~O=W?pepCzi z4F$ztNNK98&vfEHfLrQ{DIV@8&`;qDU9pf}WBX*$O}~7;z=R=fxT@MaPizo) z7gCWr<~P8FU;2%d()7B9j3E}^huI% zJUDR5F8=XS6gf5cxWjF_NJ*WMGNl0#OoX)ItR%7;X8|f@QBzjNhd~5YbJa~oC1*4s zs?pI$AR?y-MC30SE=|%Bpp#KVnoxK8W5vKHy+PPx%8Ly<;Wj(Sc-UAC@%=^bI!4@U zVMC)Ye9zWhhXblb%EVxSb%g2k+U#;*#o%JvcnvVZbjYR!vKzegY0TtEOp{7OV>4mB8V^NkkBJ=PiPr zo!`N);y_I)>qkSrCK%y=RESrT{#i3VfM4KG(JDX)0$jmFvod2>fgr$u8Xm4IUEY!r zZhX#Olf{g5pTvVRQrrG?Rr`TY6})LzHqjpY6%Hkgh-iGo5Jiu{oc>)?znu#Y>cBXC=^pDOu*rsfy?NaMzu)pLxV5>ye-x+? zEDj#BIR7u|fq%gU6H)yUyv+UEp6AG66y&`xgwr4g<$nW4Kz0OhPg>Zbo^Mcb5YJba zX7>3(Y;0^{C7}1TmYtH1I=uP2RYnmYkarK<@mhE^7J~YHg)oq~7)_tIr-UIB{3%J4 z>^vKZEUv474fY;F>?%>|bJ2T{^g?tH!TzQCEgydKR(&@h<_2&)mUEf%mBxM~07T%T zH3@|>@Jn0BhEywU`45Jkd&{*qn$QP#93X2QFWn0KJg&* zV7;!}mPizVEGxZp!nnUDG~{oX&8b=%fDxmz6>0{Wjtm{*RcU$UGXQ z9aiXroC45bR;zd4D%Uds*kcuu&6=1f>KqdE{|8brC?Z75#{e={pMkiAtwZ_kTmgB0 zg6Ytp893-~FgOV=C`0b>c@?H4LAQPIFU0fTHYkGFp!yAX2w|jhf^i200|6h{+6~@> zg>ue3rmf`!^=idW@QD5-0bMYu2>qUF-<;_Jys$sw!^{i^IsV43%wP)%SJE;tps~7A zfg6*81uir|9*M|?(MrJBZ>KkVoJSl8ci9xa3;Oxig5WmvdDlQk7+4bEDqNd32F2SC z;8TnWK7L6vgzbM^#nOz^$}jj57-v(D~JIUuvsFCPfy@o%yYTUfrLowrxJUk z6WEJ6a$uNWF0|1?47WLp{Zscp^8_Xc*O{a`QzBBY1=^78*zE2aSl-PDkBrbS7kLQaLXk6{A{>Lg644%4}+&BQ!>-)N>pa@#m`!2;lFpzoep7eC0P5)HgdygNC%yWZG zpq&fFaau0%Sb&{G8P%C@JxX9kqPPoSdf4Ztrv@+ZtlC8Y8BZjSpNDqt7yfy?y$3`p z#Vn5eww6FP`e$i}5=#3)ZhLFfo+{TL9$~blI`+=<@4Kw*K?8tC+ROcq(J?U}Q9k=R zMxj{LkM6S~ck_o3S&K}}6P#A+?Nb>2`H8R;cxe_zMB>t)cy9lMAA`S~O zs?pi$8zq`rT4{i+Ad@&{Y-+lT^j(#?&fzTgKl3(}l7XfOq?&~S{YoB23OPABz~lJ# z?dOeg5(QvBBZ(C0hFJGsqsqUJFfioj$U}ID|F&?mjt8NQ!E9pc8Q!Ac_s*3(*4M`ORn-A!7S)KStS9vuBFc$6IoBM+%JQ zrkS1mU{@lZ$5vxJL(jK@r`LiTWfd2$LX{|01zz^y7rYVVWs$i{$nLCxmwkvt2ERJ; zb%|KwA=dLP-&DQ;-B3Im2k$>=rVEg8A6$c!G?`A*a86#$mVndxkk1@D2{3+75;uAm zd)pqL`}{k|`a!3bKn^ac7m%0qZiz%ZF$>Vo_1{6=nn4y24$@6=!R#3OA4sLguDwmo zz3CP83G8%{pPYsrn)6c#0uECa&zsw`hR^@p?$O@Cg5>2 zI;ulRN@@cQu3^Z1Z*mwI8#B0Z_hT<0z0KaJ_qYvAO))lU&X`zSEs_y5Y@HMe?ju|w zk#br(pKi(*s>rkP#CT(CH1h?9eZw>P@HBq;t0)1iI`F$v;wzoszTHf~LQ_kBY`%WG zq{Qh(mNMy|6~W8erKjHb6zc7v%ExKYud8~~rXOeBW1s1J@K_yVwqp4|YlmCH3p2e{5yG-xT7)=ui1ZRBx<vQ{>8It(Fdf5sA!2W_n|+M9lvx>XaBMQ6N5~Nh zka3yVTvc?D@CQZOTGS+Zp>t>)ID#n*p?kLHuABv7bdAn346w3;65`pZ-;WYitkE(% ze8Z{I$GMg+`-g{ypu-ONr6+(4PB83#(_`|F0}hTE(t%L2+%*nA_aAW7Iue+IVPwm% zPAF=HpW1}|iSzVYJ_ZpFBHwCkZoZ*09p{@^iB%!qdsV^Nlf-Cv@(q!gfTfO~M2QV7 z=QK>!umR+yie{tEUkX87>wPQ(fi48C39&~6!++nM$dI#{9ijYX& z^*uenN4$B%WKQh`3SgbCGhHV-y)6pu2gq<~*0r=?zHPfBeQiLho$uOT;6-edf-hDxTq7rc+t${`(iF81 z*Pc@q$0%Zy_@F9W2D0ln7DM((b_0?-7lR~TG%=CX{L`N6j;wnqkaq417=>3S`LOiP zx!=GquGgFcB>oeU5}8N%QMeX)`2YS<1rY~zWHb%@pa1-y{`Z`aK})4jl|d|` zOLdiw}^9(?~z?qq<{JrYKF)G7QseRA7Yj!y76Z6!>%9<6iHsn7wXddg9>qJ=X%Zq>XJ(d_h5H4J|Et$oQ5vHqJh! zv7YXHlb1)M^G-L))X;F>IWz~DAf%xmo6|q508V7!7?~B*Oj!31_USeAWM1rBEk?hd zLunQpD!TjNh1*fx+WOJj?~&41@vmOxT@5+1NcE!4v9cm0B#ek(Tx=D$0R^@j^`=v+ ztG*z^byr%tW4*t8Y-GC+dkwla6>l4&e&@>Fs?4meu0H;Lv{YH20BB=nNye>QwYxAJ*l zWo6|tBZZ5^6%bmfk4g`O)I^%)?UHk73krt$ublAYvBQQ53B_^k%&z#5qo}>vo~FjV zWU-_7M*baO4L;(G7(mid55Ta?G^9sOd?DNqevdfa$yWP4x%3z{Rt?IA=`sX{V~48u z6})WMi3ti0cRlr}^+#90LdLyj7bQ9Lwj5F1nj>M8AX3b|4V~oDcogjVcgl%fudeie zsq3>)ryh0gjFswMo37D*Aa48cfn5E8!_$1H#)o&uBEKnTLV%@MByEL9aZ7sbtSc6O zOJuuIZqx>g;F^!CBSn4tQvKeH2;tN|;L||MM!XR%PxM`w2=aS8Xef3Q+dJ&xnJLR5 zw~ALJU%o3^kZpWhM7L$*wVCR9`>g(xejXk0z0Y+H3fhd-#nK4Oj1IjL-OIn*9!<7? zYzsPiRDqGJt%Y;A_F3*0%{wMwUz(6j-&1X}ntftc<$d_{$`xHl)4P$LOEqgCuzW1n<$HuJ;hjLG<39F4tu8wMi!8`sv+|e>mInNK4 zW9d_1Zf*Pz|rZfOexi0%&7H8tM~d7W~29(iPI zl_51vZ&an!XRqhUB-R+olZOX2X6C;nLS0dSOHU|JNpGBt-(A$aKPMbY`_uDXE1y)0 zsH>`~3VuiG=ezb9HFo+4+lBNLqz6Pwy=LM}9aur&j>lCyZU=;$wAuO-3q1fwYie$8 zcl(-{NP*No8^`;yJK}S>gK+`n7r*c@J%I9nuMr zl(kV+Pv-F8@RqlHbXy-+9ujA_9ZeU4!rH-4f1Qg>MPcLBn5o(W$+mWO&AGZFJjVT* z0Z)nNI@FsI^Qr@(@D^3@SHSFgOON2Q$20lHqHk_(Wtx$07v(EiJL%Arp$ zfrwjkj&N`40ApvImUI{y0hSxq!HII1))(DuE{-ob8v}jrPXE9HEM)?{>s#j9Nu~Ej zrd5Yq+HLn08s|FW@V04|m3Ov2ZunvCwl8%47I`k6qSsBw$A>EuNpt5Sfx@p(dRTUL zyz%^dk935%Yxan3NcVU^g z#QcCo%U~o=qEr7BqBQ4?;%B!tYnL;U|C48UneNq#7qh=-0)uDtefJG@E1pw-*B4@z zF_0tMyK#-*92#xmsXBVH^J)(-b-ZB>e}MiiturjBQ8KEwLcu6 zOzOwnGsuLkx(lkwEIx!%t@JFPI0P*9!Z(dGZf6cmeJRbD)SDH%X)}zIE~C%{_~v{- zt-7S(u7k*{$Cy$hU4 z;g3kWXMhPK4VW&lg*~o-f!?u{y?IyTu)Vc+&MfP*@`U96yL?KQCbZKhUD}3!f-Y_~ z-zxz6ywS_~2pm^k-;<(VvOfN>T?D?51G;$jv)=KX^NBVC#w$Q+;db*J*I`chNjr~@ zDJ;=fDsZ>HWmwHq;lzGEQ^-ySJx6ojd`+pWyrTUm?S=K^)EeDWyJvk_VgfdLffHxP zi~|cO2=wnqJ;Ri0hqkR02C-tu|y8lq|#&l35vU9(U%qa)}iLro_j$sIW7Vf9gkH1q8FV>N~Q_L zy1IQhq=nmbFqo^{mL<>u27}e{%Byk|f^o{RU-U{FF6RTTNK$@kyCGFCvid~Weo({2 za!}{lm1Pr^ z5X*$t-!y9?Tyz^0{0cl9*^ZHO_KEXiKJSb9CF^*B6XOH5Py6X}2d92>6+;w)c`?0b zAv@}v9A94T?xDF7vY}&Q%zP1)00sx_sdVAb{u8HzAusoZ{Id4-PUc7*?DnkDFHw&o zYIbd(hiD=UzG$YC=(!wNm7BE8#sX>mC0Q-)NajV}4%zaG@7$-;b*WxO8iRz$HxmYB!F=$U)e$w9k$%OPm~6>lEJ4 ziJ+_5%lMcvZ}CpOxG&%Yl3TynC-rY#kd&0f7QX6xg_@D)>8_Df-7@179dcnwuB*%$ z%B0FAp>po}$-Exau(l(qt8cZhXU##UPM+S0-NvNn>UfdvsrH08ck0tf@%_n}QfQ&+ z+*nl?ee|Nt$;mH3j_3K0`$;0snAroIf;o=5!xEQMylz|-wAO1C-K5-?FLMp8OMd*% zj;Ahn3`lP6YXBR8aj0qN!1VM-+{o(k>(tcvWIX6|9d8;7O>r-ikc31rw501*OT&hV z3GSoCoA!=s0cL7xDFG^)Ur)J%`gLYzLx8}PId0!-qENM|+7-VAD2_ zce?S@Jl#qzCOPWxpvp6jSJpKRmY|?~+h-M{Klk+e<@{wOB_8{I4Rnmy`VQ4l1ZCG4 z2)O+qb$wS{Bq0S6sx*M~#gCrHCOy=x+OR)acSLW-YI0Sh*VuW!efITyOkx=O85haP#@~HKD46tWA0EGcFYRqd$ihUR9N~DWWyeWq!syLH^fQ z%EY0jVNId)beRzh{QNQeNTnfF%V#q6Mu4~D?kM-v%5R3J->ytNZT`k-q?R3+h^^O* z-TUCR%#@SHr{gvD)8Bg%p7jY3uWeaFN<^}Xw%KacOyjOIQc2<-SAe=kyvcRo-L2=+?(U+H zoTED%bSZ-#n$D%chcSPA0|T+3ohyQI`eXb^>Fo3MFIuH>>4s4;{%uEl2GA4c?M(}m zVeL^xwbn;b44Qqe^#%GwKg#NA!TmO&BeY%9a$bb_;uYfh<%M$_o2`7HdBrMw@<+E^ z&7z2jlQ1RTyD@N&ftOCN<~MhL>6N{;SIW%!2Aev+KB=J@eGZqA$qymbZsOpXp8D1J z_)8iQbu#0R+KJmP1SH&@NT1CNvC$qR0QhlCNb@gU6G9cM*sIsJslA^0!M!XdPm!=O zFf58-GA>Ue=;(3OWo0F`EX4;Aw~6`mgWYTn?1^V_Zx}7TkI9a!YwV&*m#zWNz;;?n z!Z`v0ZJzc4-tmiO?a_Tf%pr zKPRH3q#`7{d>w6MheB`3q@3-#i@uj8VL(&(pD=d6@*Dc$wsXl@%j>b>|7m2hp*DFPi)f#uL02=e!XdzXQt z08t|Uq+2Qe5V{@h=h)j0&Pcfvxd$+bwvQrd_O3Yn9;X9jsa)jX=%^Tv`y!bz#*asN z@^x0AnNW^1BnM(AlM*N^Z5KB-Hyc5&c$S8jKyXvwOxwntHgo6Q?p&*|QE>5TvP(rJ zo;upG-{jO3D>8wG05+UkMXj$lwLSB^-q2vvDe(eus(!rb%s{xkVsDk>=FIR_^YSjx zzAF|Z*7M;dI^MOLs3<5t@w_t=C>Ad6$D7ZS@PvK3CEO&mNeL@zL=WVk%gCVKS6GPL z9?!_N(+exdYS<~d7e6#k!*hRAyRl$)m8Tm&ATN(QmRmoF-77Lg)sb)z!QD8Z4q@bg zl&jB+__H;)yeXBoi+&IMLt=Q{+sv+CzuxA(Ghtz%U59FIZM}u!nce?8-(}nNxco%@ zSC%e;v5cHMhXJu<&Wk8U7Fuk=J$hSaQQ7bFKZ|4}p05n!s^{so1k%)U#tly*+2q^d z$l7l{vHkUlbnZkEJty*XY$9(R_l0NOW%sRvBl~Bis{#c%iM3v|(DxI$U#2lQIM_js z)AY3-#8ex;4e6&U2WHZs=WIWj{88@#+pZB$WSL<1TaoYyx4swk1qx4~DAmW&(IlOa z2t%jFYxNxtJcQKL0R!XlW{VM}k=&ToJUa46?Bj*Cphg>E;})WyVD$A(U?y{dpp`#h zrzhK@(BcKXV%=x{An;p>3=*!>ho?p)iDY*Cz-0<>Of+tJ>Yc$6mHGLOT_oq*CI_yC zUo)>2Q8vug!N}1nMu(FSpL>;K_AZ&A)0LVN>-3~$!BRQVwktM4YM&Izx&P3hl9H+e z{i_V-*C8t-ttm~u`NodnYkW^UnAX?-@>p_{6F?%l?K$cF{*%(jTTi_B@t<}l#r8f9 z`SP%Dd4IB6p}=C`lDpV?3b`&dKRK^EWwJ+x2(*esN}L|6dUEF~zK>42B{$mGto3CY z#j}{LZ}aCHLA%*Sx^D?59XKsLw1cl2e3el7bG zi8LJ_xobuWds0;@IQMHQG9veiNU*m`jy~sFEim8>mfFPO8Ft2U2KIju zX(P-vdJvx8N&x-ex3o_noyKD~ZL!7J^f(=>os|=T?Gq`c*VSfX?{03zDx-&z=f{50 z0Ezm%YZaU|*(KI}ml#^{Alj_r9AHX5I)p~YtF3Iv6#3qWBhTB@g7{`sD z2X&Klc^u|ZU2KxKcb<>Bn#fu44;Lx?4VSrL;R9=78V<{vxX|nzO3EC0d$!tbE0VOp zIx9YxSzKJcX6gBMiz8$i8(R8FSY4~#aoe-skOD3@;HW1G=?(k&le!9@tsl*OyLfL7 z?Ze)<7h0~h_5O4(!W`Rp?C)T^)4|x-$WaiX`b7Gc_~C3d^XX%g{z=PoVyRjTYJ82E zE7Lti3l3%z84M+sL)T@JLL}Uq{ydp!m6I&W;M~|1$(w9w-rnkk^uyL!Nb!-5j_%`y zvq2Hujht+Ou!)s%jT_Q$md$7zS$l}d5{v!lof;j`_Unil}*4e7>Su+^&=R z`fN;+FTkYL%GF@^kAfC?)Q^4j>mB-dF#evC-y+|%$16N096rlOx z`}?yuNpNv-hkjilCub#G11)LaqV%f)HG8Z;+_zDW<+s5y9i@|QEv;#{a)F)_IhkN1{VGeJ z9IOv|4wIDH>8@0tgS#fa5XoG<`r5$o84A~UYjbm_YxAdqiH>Vh!P6fnCl`Z&jDLE{ z@5o+aboHcPTgt#@!Q1ns@^f9*jLJn8ANAPV=01+A`!tX$5O{$9!R|?a(vpzpYQB(c zc{0z|h@EYIsZ5-;)m$mpg2t)d+{Ct*rD|5ccl8%Br?t@#V5k%hU<|<|pk%9cU{;{Q z5~0w3zCKR+{yp9PK^$ut=^KuI5_UbD#_=amsL@mJb9&{Yh&I;zw6 zcJmltCD(O~NgoqXknm09KYPb9IgmF()|b`a05ax?qN^r4n`YI`DqNC=`o<&E1?8G5 zJu;q2U7m_S{*2@96hgaH>F`@-8nqV%taJihaGx~%Y9gWneRQDvEAVK20xGfh(ioQq zYvVH)*nt1V1FRd@LR)puQPhBJy+7Hrh~ z@I9cqmAGS2*#&b!!STK8&Mjp$i?Nlhi$uJ6C!O+0^w7=~AB z2G!3uMcLG|xGGnr8t>f2GgWbIpRQRP{fxCXRvSno)V}vGEqu`20;d2zHqu+1H_U7fZ z-z(NPXncLa<+M5)UC6r`gO*3|0sT##SHf*q!urvits!Rpbla)48(7q!Gygr;+N}Qr z#+I9laec#3;aXkGL~FTN^<=4=$l56W%ctY+EUIaEvU0LYW%J)>f`Q|KZgE_9b!Wt% zsWDi={^Te?P8z!J4B} z3<{fEJui0E?VFaQPAITH)g9U_GFG>GQThBqSDRlZ@Ypk+{r2`zns2#wJ9H5ZFyURWL^sfCmx;~7L7CLh?NjQho9yHDe1!2qB-sh1GlQVH9BK@2pOa3~# zD1}d*`p<8%3oc^ScZDYHxVUnK9CqK_RW%Wap4P|GvC+85g2K4*(p%EtWMjtJc2Azx z^7pE#;pfNcv%)uT&|mrfoohNif&Ykkb#sI<~h4%f|ZKxs-fyYEe#BQ@Va zrX9dFuH@u=VVq-h%vO1i18IhZ*?tL;vGC79%?84~Ns50bmHFC6Z=;!;x! z&pC7Ft*lP_CJNfzA7Ze+GEly!N?~^>`tbHeGVBk;8pdhqulUrhzj~#I3LVpkyVGu` zRaRp8`mVLP-md#Xu_aQh*5z7wF^W|Km!wPjxeC$4qgY~Rwq7k?8YY{L`Gm&bBPj8^ zo!8$gm**;GJQpU^q@pZrcX?K2=iE)VLUNTU{AVHAt-CRG68CRo^m|>d(L3y8oK!9& zGgtW%42@KeRHTfQ(%k8`LdzumGL#aVng+>AQLU!|;6tBlAIEI5ThO8sMc-Ut+{#2L z=5$wl0lIc>)XXVB--G}LZ=j73NAQ5@$TYQUhuX`qVa`khZIpKpzh7UE8dIHOKM zl;Y01BL7p}`PO3l#t`wA%>aSm_pm4 z;kQhBIs)C7SOjav+x_@knk~nNv48HhM}M4}ouwyipra!&)qC1;VNA_({>gmXy6*j- zsP?Y2zEK4Q*o8?Gds> zav7DVN#I<(>d@3%3*XrN)qtzL2kD*d6*@D}pvnYHz$2_;j- z&qn+Yo=)AW2S=OlRjc2Tah2VI;^wnolm}aLtrhy}(@AGT&n1<-zbIz|t#=LM08dbc z&aXXI9dzOjHQ(&S#HNcAwKOkJu=b{>{T&V~_i9BrcG_d=ynHVVkW@G=2S1AAHrkHe zQSuFPD155NZI^k&#a~;sp|P>RvGs{g)g*WA0~c8o8=9NegG5m4YoxBF+FA&3sg!+- zV{30O-7wP{O<5cEYWq{Qg{G3>EZ=_()tK_{uS!13tG=%uoQ6oWj1&*+70 zD8w2?=G}`PC)pRtoV*J+UL4QNCVaO1_H*I8dqv8AH4X7kf+C>z}0I)+)aJUQGt?09QsrTLvYzu0Q^j8-2_#AK#L?eUe z7KBPAJ_lNr;U~wn0x3Y-=%c6X-RZc;w^U*vsEN5~>sg%tHCiStGF0yEHr{rw;puD= z87ak4lShK=%k7Mh{)|_mugtCEIXG$QB?TUg2vnC3@HqDp*L81~@^^n+`>gKrYpDn( z{5Gl%DG#5ZlqKH6_-sO-B?py>p(*2zx-j>aAnyPp-nP+J)w`OEVmXriO3bPnaz(fmx{1I3BudzVpV( z(X0;qb9x>~g1Z5yo{G3Th%f%lKiX~}5zr0Y_k!o4-@m``U;c7ijX4$7LT#O{TH=4Z z{omgz#6xUHR(&!2e}AZdeCt);#ZPq!eQx*H75*~{_FuwogTmNs0zz+Z46d}vzd=;(e zk4i~POY=Z@6Yc%73H>@F<4LpnuU>Ya+K3CSe2Eb=wGjo$2YJkOb~(oTc?-wRdXftn zwXb(;&$L=vT6hEmB$brtuUxrudP49);ctWfuWM|h^s1uI4)N6#mEo^xEy+gAetMOm_ z)P|S$x{-l&b#>KtriCA%Ovk^e-gNa`w6-21ymBS!#_pCIv3FGP#kDcqI}w!B*cUI> zSEq`QbKTGeeZ9)tbfEk;bKJa*O@MnclCm)IT#{>Yp(~E&$&)7y!(*HxcN!Z_-0ges zU5ZQsfJ$>rCn+HfP0B}_u9p5w!YN@ff%OEh;^X&!uEiO@S+};`kP>ybJ^Pp}@>@>n z<&4Pcj56Lpu5L^C%D`QNgHl00kg#W4^h-UGEc4=Cd>h%wyR;lj1;KAV51)*R>`jnO z^ZW5?a>65^wY;LD{^#hO2;4FkG{PvtT9NNP?V5)OhZ`_Ml{qX3-0@8jPl<}+U8SO& zTuVmlk5vl_4!vhEY4uhL?~Y(C_n1ymV@J!isVPHSTid%PCND+1;k0#@AG$A$619JzY4C_FwFC8y6kvutp_GA>~^$zsUHq1X*(O z!Q?_IF1tFns?QGN_UV2)x1qUF231*cf79l< z8po~MW-D1Db(G7ZR7S?6Kpt8rQmX%_ko&m2Lp3Y?@kz#D=>&m@la+sPFfZmMGF0eN zUL7pJkV~uzXq35R)Kx(Yee;KnD<;m}J}4)cjSwjSYUNFLby!RDK@xSjVyd@bu1;kL zq_S$ldQWE-QfbA^CKZ8J!Dzb#A3E2`<&%V<#XqjTV;INg>(Wc)d<;p9(>bH!&!5TN zafkz|Jp?F4?8ta~db*o4H8!KodR84*B_^?It3?PYD2{Ye&eHv|RlfLO?j%f3)_V+A zST#Npa~@t%GO+xWPo5xX5$*xyh|Qk|^d%<0SfUp;zXehX+fPm3^BTpiNbHe0?XPuq zvWKMXrPlFkw|x7)=%;#>_j{({5K9aZD^WcwlK^HM_q*1Rm{7ZgwRdm2i8)lrkZ#sqg|aw<1)n3Y1btMlHE_t-Y7}X5``%;wQJc1FGMMcfvuh!f`EJhA`5${#f}Y zvObLA2nQy)sZq$`45CQnn~OBHHr4+= zZ1M3TfH>kTt!8?fddz|$AhHRNZ#)*?yP+8T_&GP}{(eEPiJ{j}q5tCG`%3KFd{5+? zXIj0&8S)j@eE^rl))GMxtemM}!jfMjX2#O-hAUDb(91hfa547CjUkPdsk*0xLBM** zyVxR*ftUBMIrPXTw^C8qfi{qpsPS(xG2YkJbnIG3UEhw~qWj)5?;<6t6YFaJC zt1mrsU8VAg*iFkHDtCjz6Z@;rtusIWYeNvXG<~9!0Ed<+!Mj^dffM7p@nQ>4k@ARF zZRwPD7T)3R`fi*}RenK6gs0V2-i%Z+AzRiQsSXxGaL5HcQKriHGodvh z7hBtkt93Jgsm~+ov^|aP?TmcT+n3S)rda+Ts!x-6P;@8Jou+o$+p~Ll`&{g-6qlN8 zLIKO~h<_mJZYkZhVR11En~3c~4w|K|$1ViD_)EP=_cD#+jbWZlv=MmEi7(E*279#( ze97egAv{+H*JY4y8{~dNoFg0C(ctd9xbLOL)(5%1AdmzBBWyagUxK8U6z8Hap8AKj z%mF`y%;(Ee4{@%qjgh>F`6(+a&p@Y*4dkr-Sx*ht3o?|+3mglcp0N3))*MsQ(?7g3 zSD>Lisk)F2w3>?IHn$O~cE;)GkYfB$&4-L^BV)y!G4wozCWMGJm^O=6r``prQ0up3 zP6*&x3A?^9_I5h>b`Ij{T%l)|*FlOw(k(uccsGkk022w_NHigOJ}2sWyedAK#UUXf z@y)>72k@Uea^w)WcH6las5+x(FBZdt4(#w4A4Qi*hQ>u*3%Reyk|&T0r(l7pSiU;_ z^+T^B7o#AB=l;dxG@!PHUiVvo)!q0kb(5IS4x`d~#t%ejnQyFSVSTkvKCAq^jImsE zM060J*bj_0+s3AzUN(5(9vx8L45WGVY<76s**l3N75jWEKc4k3rye{SICfnlQrE31 zdn%zZRbdJEc1`iiS`a4)Xi9tsO^MdiZFM^UUdNVp-DUlFLWoa&Sm$4l)dDE><^$<1 zzJZGD@i9+eH2XLH28M8npSv9b-}P(T;c8SpuLg?B-ANzQUcxS+j^lVX)yL7CN)A>!I%US=!qN%^EblQe z-(ps~9M)G;b%)$ap__;dKryheU@PqsE(rFZFU{J^K?)R{y(Zp1s94XREyiPn1AMqE ziG~3g`GJ9%5po7K4EM=Xe-(3uo`xv@H?gdm-w8k`IDn$AauMLOOk(BI5vd1r3$T{ zS!v9e;pSPvhTHyhJ`r61pcAB^($*&JpSg>R0ER7gN5s9}ddf1LY^HgYXRG_2Pf|6B z9Ttz;_{IA65O6+jKft3}njT|jR4RLUb?%`pQQ#kn08~^mJ4eU%I$NA4Y!A>*zk2>I ztHgU=X%ajh>J2}!Ak4;XhU99{Ld>IKu0S))4efLx=b6y6D1Z=A`3&l4g4X!J0X8Naj5xof2zxErB zGx>ZJGlYZLDBKP9>Pa*z+?XzWLl0YKElxRTH9CG}WpT6VH$FSAU1~r$iy;*w4@=|W zMJ}$XzBZa?av6Ekhj^h^KwD-$;9XLJt5a>KU=)oVez;=8W>f9BM(wcL#dO%+8A&Cz znD#me$0ZVKUc69GKR`ltpTeAkFwl=ZWPPBb0!e~AFcIqLJvYS5paywpM5(E%oQBI5 z0NC&beZJ6UtxPB#i&pLHbO!aJ*=4gv&(nuS8TCy8*DKBe>YgFjlrAp!B5e8%vBTy< z0i0)`6V2yw_CdDH$G72u!`dOi<2TM;p}!I5vP)u;hM#Og&(1AWD{Aj0wifb~TIymW7WeyWC%TN2r^v z<{C*ti3a5Do$b6U?^E`_srA=7*sz)DHA3+qh+ArSR3BfU-ovo_43i7QLhBezp4`WJ z+OZwPqj9?L4U~ORFwB;ln74Rem3hOhZPz6pj6sW4IqgLH-MEge%%ayV8|cF(%bXqn zt{4sSWtDz-JnBcDhwcQWfGnS`ZIHvKU6x&pA-xLSI6w*`IZwlQ8Md-BR8h=Q!cEVs zKYvrk*T-W&Ol&oE^=tI#_TY9vQUf8zv3?a^a7kzg7`?zOI3eQ;!y3r zm_VX3SU2$($j(+LFUUt7e{dh!0kS;>!otJWE{SyoZEkRwmPssilz*^mw(^?l*r~w2 zIbY>kLB9n}s(1M}l83ez2m4WN!g)6&o%bL# z`JlHT!)|*W8{iEqMn;?l&4Lc|ZGkvOt=US-`_(W@YZDeokx?js*8MO=4bIckxc0it z2ft+og@&RpP(FO!tImqYIrMDs@RDrQ)+d6Z)~I(}9-l$G>5YoLQM!nSn1n#1V(Y#q z?mX!v*6RIcCry-~dU`CU+un^P3Galhj`#?DA&?4C5tF5KAvE^w*Lrd^D$Mf35Ts+< z-;4uoFlwFEKM|{#0Me&9z(!FYTsP4`PS%(c4z!NP9XunD;G*s~pSuM6&9g8@8$Hyg zqM~LQI1>Ty66De`bb{|ybNFrT&-Y-_JvK`%e^>~zipG8%UBA7?mx{uCu>4e-YV)pVdxjWFk@lPch&^|J&t_8;kcTsH81h(ScUTo!3G zm?dGvp?QzN#}yje_@sL$Lqh+A!L6JwNQynJ#tWL@A}yEu4P@`^3C&f z>ZUXjBZQ&ZnM14MZVI~S|8kJS$2@6yOSq-0eAF^mSzURS#-gWabe&~|k ziyLqSn?ieJNZVi9RcsAPRW>vL+QD_q(ALoE(h<7MD`iy6&OSlc{_uf-UV$VOXkT)n zwh1h%q-~X#yx|J1fkqC1eqD(J}VusgPw8Zy(fJA|V>IbMZr_VKXkbO;5()^w4o zOD;*5zkiiX?0c!AqVj=JAJxMRCkl2#A*iolGT*h|ky)asB=pGh3&^Mk)IlUi&S5%U z*B)ZLSO*uE#q!7|?}i47vy0uYu#0-vhNy*K-`ZOp>}!-G97q zEaSVO6n66iaezUPV5fuGazyi_z35`s(1W859U1GXBW(L2C6~++)#<87e>g^(ki5{9 z{ehPP%3xcKq+&d>kVa3tiUjxuPg?v7KgPs^BvJo`#yLj)N_%@wph-1{{t7Nd|S}`@R$&YafZQ$0SF1fQ~?4&j?kX z{i#=&i!trkWiwZa2kd>N&TSeb`2<|I`0(*5vmDm8l#37Rs2XttpRTBD(wxk?dXWpe z;RZ%h`kC)RH>R>OSeB^-JvDeR?ViuV;d&D3yED9*$~m9F;*b~C5|bD4wqGhe^1S`< zvh7E)sbQ1Eq4LIPejv+L#-V6<;tz{pM9vR|PHq@gZ6g>FY^r@RK94H`A73h&@DCxQ zH`g$^_27fn!?&HzZo5NS9H~McVt#TDjma|#BD*hUrYp#Tg38?o&Ir6Ktfn4fJgf~B z4?GX8Rx1^IN>b-of5<&GSB3=<&}G_E>N`5^lVL2kj7HHFgoDPJha{SnXDVYSz5~Mierv!Z?>5$)%>1s zIrbxE*+944tfrcnzlfjE^Um}uUmAacvP>pp*Y!H_a^2!mG!{+cB?^$O;pMGHIR|(v zSPES6@!DJI^X+FWzouUSmbsyfO{UvYcy%>Xe#Sxjvxh6v_PiSr63c|lYRa?eDxRc) z$v{w^qE;QexY}(zS#HY0}LGH#ytw>)|S-3cc{??9zE+Y)q2ne!ly4fRl0> z4p;C)G&lc6E>cfbwU1&ns4f0}x+;}Vqo&kfjQDYMaxIdBsoa5tO{jsW$;?g`cqNUm1XG1HA}3uvdCTBG`X!ygeS? zT?7+y9jZ`KOci#2Xv_d*Uw(!tbO5v;@NztuDHmRYh7I4I{o>@xU2wl=&Bxr<{`t~j z?I@N%@%VBLP)dMOs@)v-5-!h^%1kVLbNizh7TkO6b<#q_!TBosl7oky)E^qs+CTR6 z5A7kBl`M)Tu#QgyQjKWd1tTKtCNuAi50iTs>WXDew#ePrU?DW&4wNTo$EU%^( z>M#3qb#hm|6IH1JfJCJn9XbZ!CVpNhVfbcW$!2NpeQqzboAXL%Eh{ztWT-XfD|)rx0G8?3dSfzqtm zc7RRu#?)%boGP+8_(|u%nmSb;MuDULkmTo!K_pr_`tNT2(=O!$uTup)lnI+jl4RA7 z%GN?lJlja?ug}nztCw53h!#c3L>>XxSLp>2Tf@r`m`14sCn@izJMerXBX?pGHTUqLW_tc z)!)ICxw~c`P~|c_(;1gVbY08MXzOZ3FQGr$8$jm@Gmcrkt?X%Nsna%^{bR&wUtpsL z+S_oG5cF4;W8#R%x~1j=3ig(g5>g-x%GTa8B{}JB!yAFk7ivF!kZ$MYozEJb@7bj8 zIK-$~rWi>}y(ah3&bcIseSPDQw>LN!g0&M`pTTTv3&xAI>2B3?9W|4rHMpJDcQPcb zKn`QHOygz=OY)S`Sk)ZfaYTBg*EXH&)mlw+@;*_Qdm!JIrYlrDAG*hCu3YaG*5s z0~_%Zo`JD2lVD=ZYj z;Wq1x;d$|%haXVZwJPX=>%FhAyL0#T8$19eu~DPQrwWluhQ#9{MYs0+W@xq2DqWpI zwqys`k>)&W?G)JfGw&JOP+uIxIK%oUzDtsg{?es_tizitcT*M<`|%!JH8E2&uh_Bc z*D`YcCK%?YQfUS)XSSY;cp~r9=~L!J;2162pc|q=pmFTxW;nE5Gudd2wPR&HRAFfg z#NsOC8)5sE6;;xwTTXVo2RsUC{Eq>>&$SpZy~0tk5C$Xbr*;mG&==H+eOnXOety{! z{OG835#s4p+`O@qQ#2s!etGP{1KaK!xO12hv2H#=@8)1b^p^WCnWgU+Mi=MHY-z&I zwS8nF{-J<_zjx5rhH*wb)h_0Il|Unc@0~gzvlNa@TBBsg^RgD!M0uEnkpYG%Ry21* zdYoYJav+s#*alfE780LUQPTpQ@$P$<^w{r{$zTxuz&_po%Q74 z7%5{!QK5rw2b1&uxU`?KfE-DM@akNo#5ZofIF$u-T?AzPmO6!>uY<4i$c^0VM#M~l zttsUZGBXU|Q|5-(@Kyep<)v@l`@t@vN7wA8F&Nva4iBEz*dNO%D}T4UJBcY?y>`1u zpz1!IG%v3D6bNkp4KIJ@XjFan4=M>K( zQG}76hL19>aYG=m^Q=bv(j?Io*|-;>Y-#R`cfRh4oR-N}9akqS#jBbcRXk4EYPnin zvlX98tuO#vHPEV6qtqx`LYVt;I#~j7MxS&1E5(|rAgA{Y4OqcHyw3XGK62Y`c!WCs zi7Vc{D}c7vt~-V7+r{lWrFo8ZeslL~b{j`2+>!cG=m? zxZo$IFCP{ZkO;WgQ3<(Rye5S7#uh$v>e%?8S!skv{2*m+PW;oishR24o*t=BgQ`C@ zP*bX`aL)}N2#_so_<@Lom5I||v}m!d{qHX?`k__Mo@It8SwR<-ZZM=PTv;)zZ>qp%l_dF!Z(9+?P{SLCoYs zc6IMYIV?8y?QS@ayI@Z47M8fEs_( zT4d(yjEGMW=d1Qs%nAzWww)2I7p@&9v8=jp2=AHJ-=A={UwB@^Wb*s}7!h>~wqlq` zOEbL7NZQd+n9(>c(VbEFR^qwZ^(Hv<)$a*CcU>JdUHAIuD-p&)5gjL_x(ft@>Xxdqm3A#OGpwbua3<712Avp2Tq!0Z$pIdG5O~Bi zUZ4#CXVNUqk$7eO#4t_w6QJY8cvR1dObCnZ!zd`IyE;t}4I#TNU)s+TdSDElsoD-R zWhM%3mYw+6)aYCCam39&k3?(A;ptGhwtoI)@5QwD?ZQ(cGXEHwz!-{yI4iGr|IP*Y zNH#E6DUa^T3k=pJg|3X+rK>%6f0FkEnc7$#9CR@mDaqJ5V4I%#@>v(_wBv||y|x*E z5_7pEO0X!VbMees3WFD^<4J4=y}`hq^$~wW9lOZA!ksI4)@5TqX?edgFPbG5ST)8A zGK1eRN&a5C8LQW{+?lD;*0FcIQ8F+MLAs7b#z}c&P*GXd;TN2R>w>5ZdL1TaGq#0Y zp!bIv%^H2!Tia73ho5`D>QZOmXJ;Pk-n!0V2IR!+9>=TJk61Hq1qMu`Sa(? z4!I*4MeD&T9nTIAjqRtoO}#l=o1SPi-6&0-ZBA_BEF^^8aTPZA{Fp744*Sa4HvRKP z!Ngb@iJ}%@*QUPMO?e{3S2fvxyd1JGq!#u_b$RIcyqQ0<`&4jzdQk zl_1_M-)hw0c6+~xjJ*^VX2Clu6*ZlsYT50Kjd*VJ(hS%!t zSG3*^7`(gw<}GgGO9- zN$N27^GgRk&_phxOx~p*c7Yom z9Sv58V;A~~*PkIyMA1a~;`m|D*aYYw0H3dl1Ye+2r|N7ewb z%Uj{iSC$FJupYVtw#D$zyS2^SiXwiZ52Y+}7i?YLoCZm|BNgG_u^RgX`w& zogTEdwZ4u{-0NJ1$%gzTWzKgUx1cK;aVIs~9H3TgL&J}L4ehUu`=QyuGu<$x=?7E& z6)z)d=ec(NU`3aNNU_Q!WqemcL%RfMY<6eAQbV7ztkM|W#W5#v?qs#F3$S)0hM#EU zy-TOVM0eCAO%4vu=X0#tG-X+-prHzG{$y$z%%(e&P2P#vm@N(LQw_sya@!5*n~8Wl zrM&eluW@N{&|r!PVR)KVHSpMi&Db`vLx#dMyWvii zo*rnK3TbEng*&&qYvsS>j@WFK43XR2Jtn$Y6U_dYHDU5(48094r#gzMJo*dmrth3>liWw`(4(dtpX))&VAFFO4Jzj-+nK}ro;QG_KS&( zt-3UH<(C#nLVUR>oUiik`OBQcPH|=Yb>PE-g&4@)HpscI)nD*t!fV0A3})1>t#5BZ zz*m@*)(aNoi4JP9f2l}phM~pV<~u0&c&DgTcW-q?i!;$-a9tJLHT>OvALy1+nweQk z%FuqfD-jVCWDxPd3BO>fA95`aM-ow`efd_7v(&WraQXY)WSS8lH<1t%!OO3;L4};V zh_`%pgY3sdcfK^-G$o&RN3ja7yK5ur&a4u&LLAAIstq44X5eY`s{aUcUZA9BQy+Jb zmyyLk9Pj8MRlART9#;!F_qqE(!Tqqqu|Uc0?Tp5gucav@nBzKKyThedw2L^l63i(&dZr9;dk-W5hfbCr6o2%I4&?RA=daFi0irt-EShtmpO~B1% z!;*H{qn?Ccw5>07t?cbfsY|#5l2aHoxZ}dVv!%5PvGWg2osSVM$gvs;@M2e(q%@7A zS!-9d8HL!9jenDk+oImEE09lpDTQL19%`TBNKmXJX4X7lJ-??WLg#E zGikfMK40bUS{=YBAc#vOZ|1aL92Cjd*6gTanYWmEn+O^tIX`+3InU$+c@VpRzG2)q zX`2&KxJ2#F2UFX=&+#nm^f^2UgARv93h-ZG74pyOqyR=tV)XiQSqH zulP6;iD4!`2e64;S~{U^L$E#7g#&~oF1dbr_Oo5{DkitdN_U+&`m;LAGe^{MH0+d2 z%RI_Nesj`FD}~R&4p5cmz2O>$Gm4Y8;T$Ylx9d>)NdJ`bC zNmgak+TN~#FW|7==f5XBBm5Naoww7T&lGibdCpS}%>aYJ=5xATgQZqYz~0jE6n$^E z4tZ~tHe=6Q!fKyRB&8O zHg0ku=+`u|HEI(Cd`o)V`+uB;NgRp3dVkvX?=8mvz6UKP@Ky(vk780{N;ABll!r}? zgKNpba9#T=9xfG0V3amqP~?TNwAOUFLv;LOnQ^j|CWCkuy{52u67LEx(Gb!&PLFjH ztGP0zPsBVwCQu4Buurf3s{LsioJjZihhV={BzvNZ_Me=i`STalTN` zXL@B^ZrU60Yof?#=OX%#2=+HqxnxvCXk^T^4*}*B7jwy1CqT!DW=>#W(-C0s5u1+J zM7IR;;*&&MrCKZ3SY{QZyE}8cg9FbcX(CRV=U(KeaS|dEzeX^Mt85sCn!*wZm3{+G zYG)|nKs3z3oQM|~hbfb*f28qY$`+L~Sktf``p&vF@zGDWuFCLuTpy2u1XF*(fr}6z z3Xx7tMPFU?LS8tIv+0(~w8ud&kFuo3w1OgnTbAh{u+6ll*KbCDjd09m5)l&87r_on z21Ut_z^lI|x;f9l&~bAZE0WSbH0tx&y~5nhCDu1c;NJ2}GSQDO!PYozYs``+L7Hu6 zu8E9@UGFu(cZTXZQ>Z^Q-7n1qK~H3OSYufB(YI7g|h@?Tz;B9{pmFZ4@is+LUAJc#%)3 zlrYSoPCPr72xci>_{QYsxEnr_r|4s3_kK|Kf`G*bI5P$NTF({#QmSykuD^2=w3;cE zBzGv(u8gYF&IxdAYP8J) z$yw9^M-&Dg+VB{Hh)t;3EXRVd6dWRs43nz0DAsEVpq32j33%F=P%uDZw@nYAqrGqi zYJ9HTtFzs|s_W{q4BOU!=;tAs^`}Pwnqa2gkP-D>x!E?b@UgpFb`3V(oy|nwq1fSc z+|X7Qzl_vE+85)R2j)RE=V_HW3?<5=IU4mIooe}~WF#csq+_4n-rC!YX0FVQkE?wu zF>{9eS%YAgGTy0=cc&}1j8}40!y3iaj!qw0e zOtD!4gzI1z4M09N?7kW(K;2%7X4dtN`+nEi))>`~T#mT&0VKt)a~oiNC`qhaX%$^h zqbqn0eGXc;qvXHHSG22a@ue+*=u@fjog@nCTnk$hMNnioED(G?oZPl)M!f^m(zSft zMhcmEgg3zBfZxc-c=c{7WA@woLM_(GsY%&bj&1q#&D;3CIo9N)4d8$f^ObZq(KNKxoAoN zNjX8IF(}B+ZfRnAS}IkDKwg9F@RGbBZm_~KBR0rj9!T`VUw5ioXkZlS);-kL7F_L5 zQT!z0FqB*x2pu;U?k&Ia3(YJDmR84H!5Ud1IKNMJ)!Bh}nTj3y!a3QrxMib?-j7f9 zZYL?C{J6w23&}dsL}sR;*2IdT=HG@Xtu8I|Szbv>OB>H#MDuNXt#@bLG!D3UVPZyd z0^vfed}qrQkh{N~{kTP1P+tX9)b>QmxXV7S9~@&x^@Y9m3Uoq^Lry=360y@E6QjQ9 zplafFNvswlS?O{sMng9>LsbJlxEyLVW`%Cc7s8%92m7_b$d(Cg4KQqBr)d5pZEUp73S?&@QHx1uwX;iM|x}CW(Y~ z_C?lZT#B?CZY&7ajgSY|&DkD7AQy~&9Dki!G%dQIA$f$R>gF~qNk!6NZL+MQRNk4m zlYv&|b}X#J+ze$LgzN_ORyy_a(jq5E&mtl5(oqXJ?+HydxTCMLB|hKR3|%h-aHDwe z$F<|K4jS0Aj?oCp>q0)X6hQBp(5^}_pQ(2hj>r%SF$>oY>J5u-2VuuC*Xx2q4RXZ{ z@WJgPw&B~}OW%|ij3xB+v>_?wDxH+d=Nudd@$Fl~*1&xC4W7jw+8k;|IZ#OaP^-w5 zh@%q>B4Ow+1n#($YO_W4u}XMywD^=35Xc%&UX0!1RDmE<#N1b_-H%bb^~9x$ zTscMCxt4T*yQHf z+2*d(4Cry%DoB6yk5NLYjo zCD&RfaZSw7t+ZMwKDrXn{kPOugi$m5mHX2Gq? zkr809*_6f40^JB~SGC4Sm0a*M2GE2PNk1ySHCuRdAnGio!(jzQtCfo&7o-h6(Xq(g zXmf)Syw=Ey2;B{91&dtc$;gy*l2Ch|OV)@nzzHWvV7EN~_%6z=cw;is&; zHky$>sL*!#_;lQNnK6Am35ASoG9eOZ>Wq?kYOK>=Y@{F3>dfxWHx#pMkfAWL-IU$U z&wIE_U~sU^a&MKpjcG9|XA^tgI=y1kub~}S(kkw5pl5HyO{!Ot3M)tymx#7Vr^nW)kBul>(-OMA3N{Q#&2uQ zOn$-Z)e%B;AV1^*1&CBIevXOIaD#+7ASQb9xo$BU8Lup^ZZNKdIr;K?YHr8abWXq8 zZmoO$#-w*0*ct4HaW}J8Kfw)2r!`&h9x;0p66b=dri=D}p3#VQkUcRGFdY12m^W5M)fd8GbKzaC~ zNJAIJVjS_u3ju6vng?tdZ04)!2NZ`2719FiAWm&H$9MP*-5T->_>FvSJI~H`&tsgE z*qr9c(ky3F-$V!Qhx`5!(YX=;gSnVrC6hTPC|D?`Yyg;uWOXj2!Y9?$2K8Wj{7qj+MzSYpAEkBc{uhDdo*?0qRSGn z`@c~A)a9fhof!Vgl8Ng`0B~5Nk5$N5pkLYbkv5B;G86UQCa|jDfo?1wUdYOp;lYDc zQ7^T0c0O9L89#8#yLe0~n99|8K#@1uK2UN-)A>yM<3ic;tcb%6ZUiMi?d?#u5(Muh zlgM#uDs$J@y*ZlB$06Hga(Q}XLtm6y33{e~+pxpUMTqGXMaoXj@)=wA9>I>Q-Xlva2fQ|f>^^Vv$wV#}>_v;H-?`GSX_TKnP>jYvDlv-WZ( z$f#0}4p^xcoW(t7F{^-FE<8&o=BZ@9AOguj1aPi{7V|GLiWpQmL>RZQDK`q{jA zfF19Lq7SH-$3z4S#fIXz2$tGy{xcjZfoFKzp-*yPiFyw>w3n zuO%E6%6pk6J;BAxbKuI)d*uAsOS<&2U8a~Mcv4v^!fRS28hZy)`#w-y=+p=g6`yMd zg@m+%s>DPki02=C4Wcyvcm>3dVv%>kJfu({Zyb9o<;(0eia$RnN!Of~LW&H9>bzDg zP7`n<|3m%y9>4_4EEyyy4fHZIwf6P9aM84qe<$mg6ZdMzde`uyH62!@P*o-+6;b@u zA#f%O3JxY53U4L8PsfVmwC7xAGs#NqfgJPIzE#HZfIeo%lBb%D^}6ww|!*63vF`t5P`brFTca4p2B)|KPSytCNMeG*4iUWcun%;t-W99q*WM3 zb|O9*1#7GRCv<#N%n;wzcIpV7o%Q<>|31hgilgA5NdlA+o)F2fXOC;Wa0t!JmI=9a zVtk0R=gKV1Cdf1mVjT39t0iaNtm~BPaoFw^&RlB>2>g2Uz-O1 zT5Z{WZ?ZONKt%@cN0e5S$JO`@7;msUfDCj+*E0}$l6RG49O6?F9!}9GFYnI|Gi>Y( zfCO>Rj_-BzFK62l@4od8jP4%d>|XIb{!0=&gk(L}>}XFKCLu?m5JA%hZ9zo2cFezl zcZ&oC1<8<*1#otq`>yzTSn{FMIB&w&A$hz~-GO9y7w7d=lhqnYp+IOVaq^0b^XpwG z22R4Y8I54wy>#QSeAi*O6dM7ft4IWD1g~D?0QnQ~j%dZZIVlnGy@0zV!DJH?+6ft% zcTo`go8$6T0-#rmuxzXA6c~k$|43vc$qP6SxB7~BUv|Aq@iGkQNI|&qqG(b>6BCl| z>org+wiKCSU;>W_kJC!L@uF;l9Ns3>h;S+I#ih`9MI!EYDLCRF(K$;xVbJif5a=i9 zaq_GTxN*~jh+yqq+@<>}ym|@8cniAe$M!npMk_0Ll!Wvt%ARBoq`1(dC?Z$LaY_33 z!&b5Jyhb(HPe&A+;_t`~#~2)nKa%m%pX?mwV#F_m-5^&zu<6!Gb9>CYR{!(~D}wWc z`#QqDhXoy%h=`q<6Q)Mli4ydCAZ4h*?BpD9=7)Qf*EJ)aKjBtsm+B1qVv#s-Nzvx4 z*WvZSME0HpKG3vMXXCO@j6@mT!#{A?v5x^S4QND^?zrC(F7mysN8CI{m}5`ygwaKq z8>^@oKD}hrnOASqG3uxpg)2xX*gP_pw}ZFsXv4M98sA|U)Z6O33xlSIPpkX=Aa%Mo;p$c&3`1o;PWQ>l0jV|mDY~hH|@J0@K zE1q-kejnWIxBvA&(Uc;0+P^7x$k|fv-Tl1CYBhEA3j2HYLE4nDFd!XHy!58%;A_)62OBc$?ZYHNJb^YZv|d{89+4 zdQsgHi7)^93mf`Y;?0K)j#>AkJQAi}$;Fd&f8=MV1*2FU1GQ$LYc>_)??OPy7CPu< z+LY6fpxkH*bE@^2p|>9)@f$!a#U>eNz__vo8sAYSQDCtjCzA;4o860WpMwK>>!+*M zp7`$RLx~(uk~`0)3$|I z>1!#kF0s!1XO8EYQPl+81S|LHPsezzFI9D_ykNGGM4pVd-~JZ8c^wZNZGabtBmp^g z)MnJxFTY<+7n~<2UeA%F@J58)mVBB zgA*6OKpOWdRL&s;-UDIBiqxqpqRceOnom;U9g?94nNfGO+jP^PsJvx^!TgqR$9&$O zyBW_7?BYkB_l3WIQFOCYP{b)`34yS_G*-SHGk1kKU*ZE z&Z2om%{PK_`uZO#n*YZGbW`?PZ3t;rrt6 zy&3+?k*McL4}SvHFLe<;9o^E^X)gvAPK;M&$?p%|uf%=a{+2uapKr-Oe|ArT9EJ){ z;^t-PW3fB@F4{`vA6ng$`{#;?|97tFKpF+y2#c#bVX6EMZC#9*z&M%EJ}yFy%#y5x z-g0)O6BzNlv9QDVW2i&$o<^GC++^yE?j_@7@Z?cT&tHwl2)+ZZu1 z>dArzqi~7I$bb{TiVp-yrakRjoy*0d$h-b`ss>~BS^pdNcjW6&h!;_`kH?}I;Hu_C z>MqrYze1DH(B`FN4j$N-YH4WwY`$6`dvVqN_vqGtKC;*(-#j^q0#{JzN*50NQti%* z+wyOZFA8i7OfGeI7si`6&IgNk!k3Ur7WR?={{ZUeONU-{&ZnDyPuu^mx1LKrN?%`J z)C4zGL4lS1^%=HbfKtM{FZuaQN3Ku5fV3y%2M@9dneS8kySgw4+_WED^((Lu3gr!$wf3M^C z_~XsR9Y;jx0>cNydDZq6SFVWF%uM&#oK4u;)|RP@hX8rU?QK=nf{3i$khc?BQ_bbe zaVLu2(^F|~*NA`kAK=jb!#=!ITXbQ>w<`q6pzA)ldX+UWQu8-+@qb@l7bdX= zBo0I3Urg42T>op;=4LHxDYn0z)<1vgfQ&Y-Lx+s_FX#P#o$CJ#(f=8u|8p1p{WJUj s@q^a9cbrh1r}M)5A6JNHxC9rdy+MBSl6T|&e}SL3k_r+9090/tcp prometheus +79f667cb7dc2 grafana/grafana "/run.sh" 8 minutes ago Up 8 minutes 0.0.0.0:3000->3000/tcp grafana +``` + +Navigate to `http://localhost:3000` in your web browser and use the login credentials (username=admin, password=grafana) specified in the compose file to access Grafana. It is already configured with prometheus as the default datasource. + +![page](/docs/images/grafana.png) + +Navigate to `http://localhost:9090` in your web browser to access directly the web interface of prometheus. + +Stop and remove the containers. Use `-v` to remove the volumes if looking to erase all data. +``` +$ docker compose down -v +``` diff --git a/etc/monitoring/compose.yaml b/etc/monitoring/compose.yaml new file mode 100644 index 00000000000..0ea5c32e1ab --- /dev/null +++ b/etc/monitoring/compose.yaml @@ -0,0 +1,30 @@ +services: + prometheus: + image: prom/prometheus + container_name: prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + ports: + - 9090:9090 + restart: unless-stopped + volumes: + - ./prometheus:/etc/prometheus + - prom_data:/prometheus + extra_hosts: + - "host.docker.internal:host-gateway" + + grafana: + image: grafana/grafana + container_name: grafana + ports: + - 3000:3000 + restart: unless-stopped + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=grafana + volumes: + - ./grafana/datasource:/etc/grafana/provisioning/datasources + - ./grafana/dashboards:/etc/grafana/provisioning/dashboards + - ./grafana/dashboard_definitions:/var/lib/grafana/dashboards +volumes: + prom_data: diff --git a/etc/monitoring/grafana/dashboard_definitions/openroad.json b/etc/monitoring/grafana/dashboard_definitions/openroad.json new file mode 100644 index 00000000000..767a1ef835f --- /dev/null +++ b/etc/monitoring/grafana/dashboard_definitions/openroad.json @@ -0,0 +1,148 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "interval": "2", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "ord_hpwl", + "fullMetaSearch": false, + "includeNullMetadata": true, + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Half Perimeter Wire Length Global Placement", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 40, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-2d", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "OpenROAD", + "uid": "bedke9uv3g5q8d", + "version": 6, + "weekStart": "" + } \ No newline at end of file diff --git a/etc/monitoring/grafana/dashboards/default.yml b/etc/monitoring/grafana/dashboards/default.yml new file mode 100644 index 00000000000..24b8d2c544f --- /dev/null +++ b/etc/monitoring/grafana/dashboards/default.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +providers: + - name: Default # A uniquely identifiable name for the provider + folder: Services # The folder where to place the dashboards + type: file + options: + path: + /var/lib/grafana/dashboards diff --git a/etc/monitoring/grafana/datasource/datasource.yml b/etc/monitoring/grafana/datasource/datasource.yml new file mode 100644 index 00000000000..d7b82868652 --- /dev/null +++ b/etc/monitoring/grafana/datasource/datasource.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: +- name: Prometheus + type: prometheus + url: http://prometheus:9090 + isDefault: true + access: proxy + editable: true diff --git a/etc/monitoring/prometheus/prometheus.yml b/etc/monitoring/prometheus/prometheus.yml new file mode 100644 index 00000000000..ff3b02c7129 --- /dev/null +++ b/etc/monitoring/prometheus/prometheus.yml @@ -0,0 +1,25 @@ +global: + scrape_interval: 15s + scrape_timeout: 10s + evaluation_interval: 15s + +alerting: + alertmanagers: + - static_configs: + - targets: [] # Keep this as is, or add your Alertmanager targets if you have any. + scheme: http # This is already correct + timeout: 10s # This is also fine + api_version: v2 # Changed from v1 to v2 + path_prefix: / # ADD THIS - Required for v2 + +scrape_configs: + - job_name: prometheus + honor_timestamps: true # This section is all good as is. + scrape_interval: 5s + scrape_timeout: 3s + metrics_path: /metrics + scheme: http + static_configs: + - targets: + - localhost:9090 + - host.docker.internal:8080 diff --git a/src/gpl/src/nesterovPlace.cpp b/src/gpl/src/nesterovPlace.cpp index d5e4ecd9d4f..5a831af1585 100644 --- a/src/gpl/src/nesterovPlace.cpp +++ b/src/gpl/src/nesterovPlace.cpp @@ -227,6 +227,15 @@ void NesterovPlace::init() totalBaseWireLengthCoeff += nb->getBaseWireLengthCoef(); } + std::shared_ptr registry = log_->getRegistry(); + auto& hpwl_gauge_family + = utl::BuildGauge() + .Name("ord_hpwl") + .Help("The half perimeter wire length of the block") + .Register(*registry); + auto& hpwl_gauge = hpwl_gauge_family.Add({}); + hpwl_gauge_ = &hpwl_gauge; + average_overflow_ = total_sum_overflow_ / nbVec_.size(); baseWireLengthCoef_ = totalBaseWireLengthCoeff / nbVec_.size(); updateWireLengthCoef(average_overflow_); @@ -687,6 +696,7 @@ void NesterovPlace::updateNextIter(const int iter) // Update divergence snapshot if (!npVars_.disableRevertIfDiverge) { int64_t hpwl = nbc_->getHpwl(); + hpwl_gauge_->Set(hpwl); if (hpwl < min_hpwl_ && average_overflow_unscaled_ <= 0.25) { min_hpwl_ = hpwl; diverge_snapshot_average_overflow_unscaled_ = average_overflow_unscaled_; diff --git a/src/gpl/src/nesterovPlace.h b/src/gpl/src/nesterovPlace.h index 32f58fcfdcc..2e070b359c5 100644 --- a/src/gpl/src/nesterovPlace.h +++ b/src/gpl/src/nesterovPlace.h @@ -40,6 +40,7 @@ #include "nesterovBase.h" #include "odb/dbBlockCallBackObj.h" #include "point.h" +#include "utl/prometheus/gauge.h" namespace utl { class Logger; @@ -135,6 +136,9 @@ class NesterovPlace float wireLengthCoefX_ = 0; float wireLengthCoefY_ = 0; + // observability metrics + utl::Gauge* hpwl_gauge_; + // half-parameter-wire-length int64_t prevHpwl_ = 0; diff --git a/src/utl/CMakeLists.txt b/src/utl/CMakeLists.txt index e4f4165a859..50af8fc3b84 100644 --- a/src/utl/CMakeLists.txt +++ b/src/utl/CMakeLists.txt @@ -73,6 +73,7 @@ add_library(utl_lib src/ScopedTemporaryFile.cpp src/Logger.cpp src/timer.cpp + src/prometheus/metrics_server.cpp ) target_include_directories(utl_lib diff --git a/src/utl/README.md b/src/utl/README.md index ffe7a090ec5..ffb951c85e9 100644 --- a/src/utl/README.md +++ b/src/utl/README.md @@ -36,10 +36,10 @@ man #### Options -| Switch Name | Description | -| ----- | ----- | -| `name` | Name of the command/message to query. | -| `-manpath` | Include optional path to man pages (e.g. ~/OpenROAD/docs/cat). | +| Switch Name | Description | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `name` | Name of the command/message to query. | +| `-manpath` | Include optional path to man pages (e.g. ~/OpenROAD/docs/cat). | | `-no_pager` | This flag determines whether you wish to see all of the man output at once. Default value is `False`, which shows a buffered output. | ## Example scripts @@ -64,13 +64,13 @@ tee (-file filename | -variable name) #### Options -| Switch Name | Description | -| ----- | ----- | -| `-file filename` | File to redirect output into. | -| `-variable name` | Direct output into a variable. | -| `-append` | Append to file. | -| `-quiet` | Do not send output to standard out. | -| `command` | Command to execute. | +| Switch Name | Description | +| ---------------- | ----------------------------------- | +| `-file filename` | File to redirect output into. | +| `-variable name` | Direct output into a variable. | +| `-append` | Append to file. | +| `-quiet` | Do not send output to standard out. | +| `command` | Command to execute. | ## Example scripts @@ -95,6 +95,32 @@ Simply run the following script: ./test/regression ``` +## Prometheus Metrics + +OpenROAD includes a metrics endpoint server that can track internal tool metrics over time. + +![page](/docs/images/grafana.png) + +To use this feature you need to do the following start the prometheus and grafana collectors + +[Detailed instructions](/etc/monitoring/README.md): +```shell +$ cd etc/monitoring +$ docker compose up -d +``` + +This will start a grafana endpoint ready to collect from the OpenROAD application you would +like to track. By default it's looking for an http server running on port 8080 on your localhost. + +To start the metrics endpoint in OpenROAD, run: +```tcl +utl::startPrometheusEndpoint 8080 +``` + +This is all configurable in the docker compose file, and you should be able to access grafana by going to +http://localhost:3000 username: admin, password: grafana. Go to the dashboard tab and click service, +then OpenROAD to see the pre-made dashboard. + ## Limitations ## FAQs diff --git a/src/utl/include/utl/Logger.h b/src/utl/include/utl/Logger.h index fcad815d7d7..7be611c3eae 100644 --- a/src/utl/include/utl/Logger.h +++ b/src/utl/include/utl/Logger.h @@ -59,6 +59,9 @@ namespace utl { +class PrometheusMetricsServer; +class Registry; + // Keep this sorted #define FOREACH_TOOL(X) \ X(ANT) \ @@ -234,6 +237,11 @@ class Logger return (it != groups.end() && level <= it->second); } + void startPrometheusEndpoint(uint16_t port); + std::shared_ptr getRegistry(); + bool isPrometheusServerReadyToServe(); + uint16_t getPrometheusPort(); + void suppressMessage(ToolId tool, int id); void unsuppressMessage(ToolId tool, int id); @@ -350,6 +358,10 @@ class Logger std::unique_ptr string_redirect_; std::unique_ptr file_redirect_; + // Prometheus server metrics collection + std::shared_ptr prometheus_registry_ = nullptr; + std::unique_ptr prometheus_metrics_ = nullptr; + // This matrix is pre-allocated so it can be safely updated // from multiple threads without locks. using MessageCounter = std::array; diff --git a/src/utl/include/utl/prometheus/atomic_floating.h b/src/utl/include/utl/prometheus/atomic_floating.h new file mode 100644 index 00000000000..501df048b1c --- /dev/null +++ b/src/utl/include/utl/prometheus/atomic_floating.h @@ -0,0 +1,77 @@ +// MIT License + +// Copyright (c) 2021 biaks (ianiskr@gmail.com) + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include +#include + +namespace utl { + +template +inline std::atomic& atomic_add_for_floating_types( + std::atomic& value, + const FloatingType& add) +{ + FloatingType desired; + FloatingType expected = value.load(std::memory_order_relaxed); + do { + desired = expected + add; + } while (!value.compare_exchange_weak(expected, desired)); + return value; +} + +template ::value, int>::type> +inline std::atomic& operator++(std::atomic& value) +{ + return atomic_add_for_floating_types(value, 1.0); +} + +template ::value, int>::type> +inline std::atomic& operator+=(std::atomic& value, + const FloatingType& val) +{ + return atomic_add_for_floating_types(value, val); +} + +template ::value, int>::type> +inline std::atomic& operator--(std::atomic& value) +{ + return atomic_add_for_floating_types(value, -1.0); +} + +template ::value, int>::type> +inline std::atomic& operator-=(std::atomic& value, + const FloatingType& val) +{ + return atomic_add_for_floating_types(value, -val); +} + +} // namespace utl diff --git a/src/utl/include/utl/prometheus/benchmark.h b/src/utl/include/utl/prometheus/benchmark.h new file mode 100644 index 00000000000..dda5c6c7644 --- /dev/null +++ b/src/utl/include/utl/prometheus/benchmark.h @@ -0,0 +1,94 @@ +// MIT License + +// Copyright (c) 2021 biaks (ianiskr@gmail.com) + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include + +#include "family.h" +#include "prometheus_metric.h" + +namespace utl { + +class Benchmark : public PrometheusMetric +{ +#ifndef NDEBUG + bool already_started = false; +#endif + + std::chrono::time_point start_; + std::chrono::time_point::duration elapsed + = std::chrono::time_point::duration:: + zero(); // elapsed time + + public: + using Value = double; + using Family = CustomFamily; + + static const PrometheusMetric::Type static_type + = PrometheusMetric::Type::Counter; + + Benchmark() : PrometheusMetric(PrometheusMetric::Type::Counter) {} + + void start() + { +#ifndef NDEBUG + if (already_started) + throw std::runtime_error("try to start already started counter"); + else + already_started = true; +#endif + + start_ = std::chrono::high_resolution_clock::now(); + } + + void stop() + { +#ifndef NDEBUG + if (already_started == false) + throw std::runtime_error("try to stop already stoped counter"); +#endif + + std::chrono::time_point stop; + stop = std::chrono::high_resolution_clock::now(); + elapsed += stop - start_; + +#ifndef NDEBUG + already_started = false; +#endif + } + + double Get() const + { + return std::chrono::duration_cast>(elapsed) + .count(); + } + + virtual ClientMetric Collect() const + { + ClientMetric metric; + metric.counter.value = Get(); + return metric; + } +}; + +} // namespace utl diff --git a/src/utl/include/utl/prometheus/builder.h b/src/utl/include/utl/prometheus/builder.h new file mode 100644 index 00000000000..ddc6d4f3b80 --- /dev/null +++ b/src/utl/include/utl/prometheus/builder.h @@ -0,0 +1,61 @@ +// MIT License + +// Copyright (c) 2021 biaks (ianiskr@gmail.com) + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include +#include + +#include "registry.h" + +namespace utl { + +template +class Builder +{ + Family::Labels labels_; + std::string name_; + std::string help_; + + public: + Builder& Labels(const std::map& labels) + { + labels_ = labels; + return *this; + } + Builder& Name(const std::string& name) + { + name_ = name; + return *this; + } + Builder& Help(const std::string& help) + { + help_ = help; + return *this; + } + CustomFamily& Register(Registry& registry) + { + return registry.Add>(name_, help_, labels_); + } +}; + +} // namespace utl \ No newline at end of file diff --git a/src/utl/include/utl/prometheus/ckms_quantiles.h b/src/utl/include/utl/prometheus/ckms_quantiles.h new file mode 100644 index 00000000000..57daf1f7e8b --- /dev/null +++ b/src/utl/include/utl/prometheus/ckms_quantiles.h @@ -0,0 +1,224 @@ +// MIT License + +// Copyright (c) 2021 biaks (ianiskr@gmail.com) + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace utl { + +namespace detail { + +class CKMSQuantiles +{ + public: + struct Quantile + { + double quantile; + double error; + double u; + double v; + + Quantile(double quantile, double error) + : quantile(quantile), + error(error), + u(2.0 * error / (1.0 - quantile)), + v(2.0 * error / quantile) + { + } + }; + + private: + struct Item + { + double value; + int g; + int delta; + + Item(double value, int lower_delta, int delta) + : value(value), g(lower_delta), delta(delta) + { + } + }; + + public: + explicit CKMSQuantiles(const std::vector& quantiles) + : quantiles_(quantiles), count_(0), buffer_{}, buffer_count_(0) + { + } + + void insert(double value) + { + buffer_[buffer_count_] = value; + ++buffer_count_; + + if (buffer_count_ == buffer_.size()) { + insertBatch(); + compress(); + } + } + + double get(double q) + { + insertBatch(); + compress(); + + if (sample_.empty()) { + return std::numeric_limits::quiet_NaN(); + } + + int rankMin = 0; + const auto desired = static_cast(q * static_cast(count_)); + const auto bound = desired + (allowableError(desired) / 2); + + auto it = sample_.begin(); + decltype(it) prev; + auto cur = it++; + + while (it != sample_.end()) { + prev = cur; + cur = it++; + + rankMin += prev->g; + + if (rankMin + cur->g + cur->delta > bound) { + return prev->value; + } + } + + return sample_.back().value; + } + + void reset() + { + count_ = 0; + sample_.clear(); + buffer_count_ = 0; + } + + private: + double allowableError(int rank) + { + auto size = sample_.size(); + double minError = static_cast(size + 1); + + for (const auto& q : quantiles_.get()) { + double error; + if (static_cast(rank) <= q.quantile * static_cast(size)) { + error = q.u * static_cast(size - rank); + } else { + error = q.v * rank; + } + if (error < minError) { + minError = error; + } + } + + return minError; + } + + bool insertBatch() + { + if (buffer_count_ == 0) { + return false; + } + + std::sort(buffer_.begin(), buffer_.begin() + buffer_count_); + + std::size_t start = 0; + if (sample_.empty()) { + sample_.emplace_back(buffer_[0], 1, 0); + ++start; + ++count_; + } + + std::size_t idx = 0; + std::size_t item = idx++; + + for (std::size_t i = start; i < buffer_count_; ++i) { + double v = buffer_[i]; + while (idx < sample_.size() && sample_[item].value < v) { + item = idx++; + } + + if (sample_[item].value > v) { + --idx; + } + + int delta; + if (idx - 1 == 0 || idx + 1 == sample_.size()) { + delta = 0; + } else { + delta = static_cast( + std::floor(allowableError(static_cast(idx + 1)))) + + 1; + } + + sample_.emplace(sample_.begin() + idx, v, 1, delta); + count_++; + item = idx++; + } + + buffer_count_ = 0; + return true; + } + + void compress() + { + if (sample_.size() < 2) { + return; + } + + std::size_t idx = 0; + std::size_t prev; + std::size_t next = idx++; + + while (idx < sample_.size()) { + prev = next; + next = idx++; + + if (sample_[prev].g + sample_[next].g + sample_[next].delta + <= allowableError(static_cast(idx - 1))) { + sample_[next].g += sample_[prev].g; + sample_.erase(sample_.begin() + prev); + } + } + } + + private: + const std::reference_wrapper> quantiles_; + + std::size_t count_; + std::vector sample_; + std::array buffer_; + std::size_t buffer_count_; +}; + +} // namespace detail + +} // namespace utl diff --git a/src/utl/include/utl/prometheus/client_metric.h b/src/utl/include/utl/prometheus/client_metric.h new file mode 100644 index 00000000000..f5eb1577113 --- /dev/null +++ b/src/utl/include/utl/prometheus/client_metric.h @@ -0,0 +1,126 @@ +// MIT License + +// Copyright (c) 2021 biaks (ianiskr@gmail.com) + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include +#include +#include +#include +#include + +namespace utl { + +struct ClientMetric +{ + // Label + + struct Label + { + std::string name; + std::string value; + + Label(std::string name_, std::string value_) + : name(std::move(name_)), value(std::move(value_)) + { + } + + bool operator<(const Label& rhs) const + { + return std::tie(name, value) < std::tie(rhs.name, rhs.value); + } + + bool operator==(const Label& rhs) const + { + return std::tie(name, value) == std::tie(rhs.name, rhs.value); + } + }; + + std::vector

; + + static const PrometheusMetric::Type static_type + = PrometheusMetric::Type::Summary; + + using Quantiles = std::vector; + + const Quantiles quantiles_; + mutable std::mutex mutex_; + std::uint64_t count_; + double sum_; + detail::TimeWindowQuantiles quantile_values_; + + public: + /// \brief Create a summary metric. + /// + /// \param quantiles A list of 'targeted' Phi-quantiles. A targeted + /// Phi-quantile is specified in the form of a Phi-quantile and tolerated + /// error. For example a Quantile{0.5, 0.1} means that the median (= 50th + /// percentile) should be returned with 10 percent error or a Quantile{0.2, + /// 0.05} means the 20th percentile with 5 percent tolerated error. Note that + /// percentiles and quantiles are the same concept, except percentiles are + /// expressed as percentages. The Phi-quantile must be in the interval [0, 1]. + /// Note that a lower tolerated error for a Phi-quantile results in higher + /// usage of resources (memory and cpu) to calculate the summary. + /// + /// The Phi-quantiles are calculated over a sliding window of time. The + /// sliding window of time is configured by max_age and age_buckets. + /// + /// \param max_age Set the duration of the time window, i.e., how long + /// observations are kept before they are discarded. The default value is 60 + /// seconds. + /// + /// \param age_buckets Set the number of buckets of the time window. It + /// determines the number of buckets used to exclude observations that are + /// older than max_age from the summary, e.g., if max_age is 60 seconds and + /// age_buckets is 5, buckets will be switched every 12 seconds. The value is + /// a trade-off between resources (memory and cpu for maintaining the bucket) + /// and how smooth the time window is moved. With only one age bucket it + /// effectively results in a complete reset of the summary each time max_age + /// has passed. The default value is 5. + Summary(const Quantiles& quantiles, + std::chrono::milliseconds max_age = std::chrono::seconds{60}, + int age_buckets = 5) + : PrometheusMetric(static_type), + quantiles_{quantiles}, + count_{0}, + sum_{0}, + quantile_values_(quantiles_, max_age, age_buckets) + { + } + + /// \brief Observe the given amount. + void Observe(const double value) + { + std::lock_guard lock(mutex_); + + count_ += 1; + sum_ += value; + quantile_values_.insert(value); + } + + /// \brief Get the current value of the summary. + /// + /// Collect is called by the Registry when collecting metrics. + virtual ClientMetric Collect() const + { + auto metric = ClientMetric{}; + + std::lock_guard lock(mutex_); + + metric.summary.quantile.reserve(quantiles_.size()); + for (const auto& quantile : quantiles_) { + auto metricQuantile = ClientMetric::Quantile{}; + metricQuantile.quantile = quantile.quantile; + metricQuantile.value = quantile_values_.get(quantile.quantile); + metric.summary.quantile.push_back(std::move(metricQuantile)); + } + metric.summary.sample_count = count_; + metric.summary.sample_sum = sum_; + + return metric; + } +}; + +/// \brief Return a builder to configure and register a Summary metric. +/// +/// @copydetails Family<>::Family() +/// +/// Example usage: +/// +/// \code +/// auto registry = std::make_shared(); +/// auto& summary_family = utl::BuildSummary() +/// .Name("some_name") +/// .Help("Additional description.") +/// .Labels({{"key", "value"}}) +/// .Register(*registry); +/// +/// ... +/// \endcode +/// +/// \return An object of unspecified type T, i.e., an implementation detail +/// except that it has the following members: +/// +/// - Name(const std::string&) to set the metric name, +/// - Help(const std::string&) to set an additional description. +/// - Label(const std::map&) to assign a set of +/// key-value pairs (= labels) to the metric. +/// +/// To finish the configuration of the Summary metric register it with +/// Register(Registry&). +using BuildSummary = Builder; + +} // namespace utl diff --git a/src/utl/include/utl/prometheus/text_serializer.h b/src/utl/include/utl/prometheus/text_serializer.h new file mode 100644 index 00000000000..b0391da00cd --- /dev/null +++ b/src/utl/include/utl/prometheus/text_serializer.h @@ -0,0 +1,264 @@ +// MIT License + +// Copyright (c) 2021 biaks (ianiskr@gmail.com) + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include + +#include +#include +#include +#include + +#include "metric_family.h" + +#if __cpp_lib_to_chars >= 201611L +#include +#endif + +namespace utl { + +class TextSerializer +{ + // Write a double as a string, with proper formatting for infinity and NaN + static void WriteValue(std::ostream& out, double value) + { + if (std::isnan(value)) + out << "Nan"; + else if (std::isinf(value)) + out << (value < 0 ? "-Inf" : "+Inf"); + + else { + std::array buffer; + +#if __cpp_lib_to_chars >= 201611L + auto [ptr, ec] + = std::to_chars(buffer.data(), buffer.data() + buffer.size(), value); + if (ec != std::errc()) { + throw std::runtime_error("Could not convert double to string: " + + std::make_error_code(ec).message()); + } + out.write(buffer.data(), ptr - buffer.data()); +#else + int wouldHaveWritten + = std::snprintf(buffer.data(), + buffer.size(), + "%.*g", + std::numeric_limits::max_digits10 - 1, + value); + if (wouldHaveWritten <= 0 + || static_cast(wouldHaveWritten) >= buffer.size()) { + throw std::runtime_error("Could not convert double to string"); + } + out.write(buffer.data(), wouldHaveWritten); +#endif + } + } + + static void WriteValue(std::ostream& out, const std::string& value) + { + for (auto c : value) { + switch (c) { + case '\n': + out << '\\' << 'n'; + break; + case '\\': + out << '\\' << c; + break; + case '"': + out << '\\' << c; + break; + default: + out << c; + break; + } + } + } + + // Write a line header: metric name and labels + template + static void WriteHead(std::ostream& out, + const MetricFamily& family, + const ClientMetric& metric, + const std::string& suffix = "", + const std::string& extraLabelName = "", + const T& extraLabelValue = T()) + { + out << family.name << suffix; + + if (!metric.label.empty() || !extraLabelName.empty()) { + out << "{"; + const char* prefix = ""; + + for (auto& lp : metric.label) { + out << prefix << lp.name << "=\""; + WriteValue(out, lp.value); + out << "\""; + prefix = ","; + } + if (!extraLabelName.empty()) { + out << prefix << extraLabelName << "=\""; + WriteValue(out, extraLabelValue); + out << "\""; + } + out << "}"; + } + out << " "; + } + + // Write a line trailer: timestamp + static void WriteTail(std::ostream& out, const ClientMetric& metric) + { + if (metric.timestamp_ms != 0) { + out << " " << metric.timestamp_ms; + } + out << "\n"; + } + + static void SerializeCounter(std::ostream& out, + const MetricFamily& family, + const ClientMetric& metric) + { + WriteHead(out, family, metric); + WriteValue(out, metric.counter.value); + WriteTail(out, metric); + } + + static void SerializeGauge(std::ostream& out, + const MetricFamily& family, + const ClientMetric& metric) + { + WriteHead(out, family, metric); + WriteValue(out, metric.gauge.value); + WriteTail(out, metric); + } + + static void SerializeSummary(std::ostream& out, + const MetricFamily& family, + const ClientMetric& metric) + { + auto& sum = metric.summary; + WriteHead(out, family, metric, "_count"); + out << sum.sample_count; + WriteTail(out, metric); + + WriteHead(out, family, metric, "_sum"); + WriteValue(out, sum.sample_sum); + WriteTail(out, metric); + + for (auto& q : sum.quantile) { + WriteHead(out, family, metric, "", "quantile", q.quantile); + WriteValue(out, q.value); + WriteTail(out, metric); + } + } + + static void SerializeUntyped(std::ostream& out, + const MetricFamily& family, + const ClientMetric& metric) + { + WriteHead(out, family, metric); + WriteValue(out, metric.untyped.value); + WriteTail(out, metric); + } + + static void SerializeHistogram(std::ostream& out, + const MetricFamily& family, + const ClientMetric& metric) + { + auto& hist = metric.histogram; + WriteHead(out, family, metric, "_count"); + out << hist.sample_count; + WriteTail(out, metric); + + WriteHead(out, family, metric, "_sum"); + WriteValue(out, hist.sample_sum); + WriteTail(out, metric); + + double last = -std::numeric_limits::infinity(); + for (auto& b : hist.bucket) { + WriteHead(out, family, metric, "_bucket", "le", b.upper_bound); + last = b.upper_bound; + out << b.cumulative_count; + WriteTail(out, metric); + } + + if (last != std::numeric_limits::infinity()) { + WriteHead(out, family, metric, "_bucket", "le", "+Inf"); + out << hist.sample_count; + WriteTail(out, metric); + } + } + + static void SerializeFamily(std::ostream& out, const MetricFamily& family) + { + if (!family.help.empty()) { + out << "# HELP " << family.name << " " << family.help << "\n"; + } + switch (family.type) { + case PrometheusMetric::Type::Counter: + out << "# TYPE " << family.name << " counter\n"; + for (auto& metric : family.metric) { + SerializeCounter(out, family, metric); + } + break; + case PrometheusMetric::Type::Gauge: + out << "# TYPE " << family.name << " gauge\n"; + for (auto& metric : family.metric) { + SerializeGauge(out, family, metric); + } + break; + case PrometheusMetric::Type::Summary: + out << "# TYPE " << family.name << " summary\n"; + for (auto& metric : family.metric) { + SerializeSummary(out, family, metric); + } + break; + case PrometheusMetric::Type::Untyped: + out << "# TYPE " << family.name << " untyped\n"; + for (auto& metric : family.metric) { + SerializeUntyped(out, family, metric); + } + break; + case PrometheusMetric::Type::Histogram: + out << "# TYPE " << family.name << " histogram\n"; + for (auto& metric : family.metric) { + SerializeHistogram(out, family, metric); + } + break; + } + } + + public: + static void Serialize(std::ostream& out, + const std::vector& metrics) + { + std::locale saved_locale = out.getloc(); + out.imbue(std::locale::classic()); + for (auto& family : metrics) { + SerializeFamily(out, family); + } + out.imbue(saved_locale); + } +}; + +} // namespace utl diff --git a/src/utl/include/utl/prometheus/time_window_quantiles.h b/src/utl/include/utl/prometheus/time_window_quantiles.h new file mode 100644 index 00000000000..25f1d17d5ee --- /dev/null +++ b/src/utl/include/utl/prometheus/time_window_quantiles.h @@ -0,0 +1,90 @@ +// MIT License + +// Copyright (c) 2021 biaks (ianiskr@gmail.com) + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include +#include +#include + +#include "ckms_quantiles.h" + +namespace utl { +namespace detail { + +class TimeWindowQuantiles +{ + using Clock = std::chrono::steady_clock; + + public: + TimeWindowQuantiles(const std::vector& quantiles, + const Clock::duration max_age, + const int age_buckets) + : quantiles_(quantiles), + ckms_quantiles_(age_buckets, CKMSQuantiles(quantiles_)), + current_bucket_(0), + last_rotation_(Clock::now()), + rotation_interval_(max_age / age_buckets) + { + } + + double get(double q) const + { + CKMSQuantiles& current_bucket = rotate(); + return current_bucket.get(q); + } + + void insert(double value) + { + rotate(); + for (auto& bucket : ckms_quantiles_) { + bucket.insert(value); + } + } + + private: + CKMSQuantiles& rotate() const + { + auto delta = Clock::now() - last_rotation_; + while (delta > rotation_interval_) { + ckms_quantiles_[current_bucket_].reset(); + + if (++current_bucket_ >= ckms_quantiles_.size()) { + current_bucket_ = 0; + } + + delta -= rotation_interval_; + last_rotation_ += rotation_interval_; + } + return ckms_quantiles_[current_bucket_]; + } + + const std::vector& quantiles_; + mutable std::vector ckms_quantiles_; + mutable std::size_t current_bucket_; + + mutable Clock::time_point last_rotation_; + const Clock::duration rotation_interval_; +}; + +} // namespace detail +} // namespace utl diff --git a/src/utl/src/Logger.cpp b/src/utl/src/Logger.cpp index 8c7dca5f3a2..434b54001a2 100644 --- a/src/utl/src/Logger.cpp +++ b/src/utl/src/Logger.cpp @@ -44,6 +44,8 @@ #include "spdlog/sinks/ostream_sink.h" #include "spdlog/sinks/stdout_color_sinks.h" #include "spdlog/spdlog.h" +#include "utl/prometheus/metrics_server.h" +#include "utl/prometheus/registry.h" namespace utl { @@ -69,6 +71,8 @@ Logger::Logger(const char* log_filename, const char* metrics_filename) counter = 0; } } + + prometheus_registry_ = std::make_shared(); } Logger::~Logger() @@ -320,6 +324,39 @@ void Logger::restoreFromRedirect() logger_->sinks().begin(), sinks_.begin(), sinks_.end()); } +void Logger::startPrometheusEndpoint(uint16_t port) +{ + if (prometheus_metrics_) { + return; + } + + prometheus_metrics_ = std::make_unique( + prometheus_registry_, this, port); +} + +std::shared_ptr Logger::getRegistry() +{ + return prometheus_registry_; +} + +bool Logger::isPrometheusServerReadyToServe() +{ + if (!prometheus_metrics_) { + return false; + } + + return prometheus_metrics_->is_ready() && prometheus_metrics_->port() != 0; +} + +uint16_t Logger::getPrometheusPort() +{ + if (!prometheus_metrics_) { + return 0; + } + + return prometheus_metrics_->port(); +} + void Logger::setFormatter() { // create formatter without a newline diff --git a/src/utl/src/Logger.i b/src/utl/src/Logger.i index 5e9746f7e17..250e5cefd1f 100644 --- a/src/utl/src/Logger.i +++ b/src/utl/src/Logger.i @@ -58,6 +58,7 @@ using ord::getLogger; // Catch exceptions in inline functions. %include "../../Exception.i" +%include "stdint.i" %import %include "LoggerCommon.h" @@ -126,6 +127,12 @@ std::string redirectStringEnd() return logger->redirectStringEnd(); } +void startPrometheusEndpoint(uint16_t port) +{ + utl::Logger* logger = ord::getLogger(); + logger->startPrometheusEndpoint(port); +} + } // namespace %} // inline diff --git a/src/utl/src/prometheus/metrics_server.cpp b/src/utl/src/prometheus/metrics_server.cpp new file mode 100644 index 00000000000..bc89f56e825 --- /dev/null +++ b/src/utl/src/prometheus/metrics_server.cpp @@ -0,0 +1,157 @@ + +// Copyright 2025 Google LLC +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "utl/prometheus/metrics_server.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utl/Logger.h" + +namespace utl { + +PrometheusMetricsServer::~PrometheusMetricsServer() +{ + using tcp = boost::asio::ip::tcp; + shutdown_ = true; + + // Make a dummy connection to unblock the accept(). + if (is_ready_) { // Only connect if the server was actually started. + try { + boost::asio::io_context + io_context; // Use a separate io_context for the connection. + boost::asio::ip::tcp::socket socket(io_context); + boost::asio::ip::tcp::endpoint endpoint( + boost::asio::ip::address::from_string("127.0.0.1"), port_); + socket.connect(endpoint); // This will unblock the accept(). + } catch (const std::exception& e) { /*Do nothing, we're dying*/ + } + } + worker_thread_.join(); +} + +std::string SnapshotPrometheusMetrics(Registry* registry) +{ + if (registry) { + std::stringstream stringstream; + TextSerializer::Serialize(stringstream, registry->Collect()); + return stringstream.str(); + } + + return "# Registry uninitialized"; +} + +boost::beast::http::response HandleRequest( + boost::beast::http::request& request, + boost::asio::ip::tcp::socket& socket, + Registry* registry) +{ + namespace http = boost::beast::http; + + // Prepare the response message + http::response response; + response.version(request.version()); + response.set(http::field::server, "OpenROAD <3"); + response.set(http::field::content_type, "text/plain"); + if (request.target() == "/metrics") { + response.result(http::status::ok); + response.body() = SnapshotPrometheusMetrics(registry); + } else { + response.result(http::status::not_found); + response.body() = "Not Found"; + } + response.content_length(response.body().size()); + response.prepare_payload(); + + return response; +} + +void PrometheusMetricsServer::RunServer() +{ + using tcp = boost::asio::ip::tcp; + namespace http = boost::beast::http; + + boost::asio::io_context io_context; + boost::asio::ip::tcp::acceptor acceptor(io_context, + {boost::asio::ip::tcp::v4(), port_}); + boost::system::error_code ec; // Create error_code outside the loop. + + // Set the port in case of user passing 0, which lets the OS choose + // the port. + port_ = acceptor.local_endpoint().port(); + is_ready_ = true; + + logger_.load()->info(utl::UTL, + 104, + "Starting Prometheus collection endpoint: " + "http://localhost:{}/metrics", + port_); + + while (!shutdown_) { + tcp::socket socket(io_context); + acceptor.accept(socket, ec); + + if (ec) { + logger_.load()->warn( + utl::UTL, 105, "Metrics server accept error: {}", ec.message()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; // Skip to the next iteration + } + + // Read the HTTP request + boost::beast::flat_buffer buffer; + http::request request; + boost::beast::http::read(socket, buffer, request, ec); + + if (ec) { + logger_.load()->warn( + utl::UTL, 106, "Metrics server read error: {}", ec.message()); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; // Skip to the next iteration + } + + // Handle the request + boost::beast::http::response response + = HandleRequest(request, socket, registry_ptr_.get()); + + // Send the response to the client + boost::beast::http::write(socket, response, ec); + + // Close the socket + if (socket.is_open()) { + socket.shutdown(tcp::socket::shutdown_send, ec); + if (ec) { + logger_.load()->warn(utl::UTL, 107, "Shutdown error: {}", ec.message()); + } + socket.close(ec); + if (ec) { + logger_.load()->warn( + utl::UTL, 108, "Socket close error: {}", ec.message()); + } + } + } +} + +void PrometheusMetricsServer::WorkerFunction() +{ + while (!shutdown_) { + try { + RunServer(); + } catch (const std::exception& e) { + logger_.load()->warn( + utl::UTL, 103, "Prometheus Server Exception: {}", e.what()); + } + } +} + +} // namespace utl \ No newline at end of file diff --git a/src/utl/test/cpp/CMakeLists.txt b/src/utl/test/cpp/CMakeLists.txt index 697a691cf1b..bc21c11bd89 100644 --- a/src/utl/test/cpp/CMakeLists.txt +++ b/src/utl/test/cpp/CMakeLists.txt @@ -3,6 +3,7 @@ include("openroad") set(TEST_LIBS GTest::gtest GTest::gtest_main + GTest::gmock utl_lib ) diff --git a/src/utl/test/cpp/TestCFileUtils.cpp b/src/utl/test/cpp/TestCFileUtils.cpp index a3fc522ea36..e5e093fb1fc 100644 --- a/src/utl/test/cpp/TestCFileUtils.cpp +++ b/src/utl/test/cpp/TestCFileUtils.cpp @@ -30,15 +30,54 @@ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. +#include +#include +#include #include #include #include +#include "gmock/gmock.h" #include "gtest/gtest.h" #include "utl/CFileUtils.h" #include "utl/ScopedTemporaryFile.h" +#include "utl/prometheus/gauge.h" namespace utl { +using ::testing::HasSubstr; + +namespace { + +// Helper function to make an HTTP request and return the response body. +std::string MakeHttpRequest(const std::string& host, + const std::string& port, + const std::string& target) +{ + boost::asio::io_context io_context; + boost::asio::ip::tcp::resolver resolver(io_context); + boost::asio::ip::tcp::socket socket(io_context); + + auto const results = resolver.resolve(host, port); + boost::asio::connect(socket, results.begin(), results.end()); + + // HTTP 1.1 request + boost::beast::http::request req{ + boost::beast::http::verb::get, target, /*version=*/11}; + req.set(boost::beast::http::field::host, host); + req.set(boost::beast::http::field::user_agent, "BoostBeastTestClient"); + + boost::beast::http::write(socket, req); + + boost::beast::flat_buffer buffer; + boost::beast::http::response res; + boost::beast::http::read(socket, buffer, res); + + socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both); + + return res.body(); +} + +} // namespace TEST(Utl, read_all_of_empty_file) { @@ -208,4 +247,34 @@ TEST(Utl, file_handler_exception_handling) } } +TEST(Utl, metrics_server_responds_with_basic_metric) +{ + Logger logger; + logger.startPrometheusEndpoint(0); + std::shared_ptr registry = logger.getRegistry(); + auto& test_gauge_family = BuildGauge() + .Name("test_gauge") + .Help("A test gauge for testing") + .Register(*registry); + auto& test_gauge = test_gauge_family.Add({}); + test_gauge.Set(10101); + + std::time_t t = std::time(0); + while (true) { + // Timeout after 10 seconds + if ((std::time(0) - t) > 10) { + EXPECT_LT((std::time(0) - t), 10); + } + + if (logger.isPrometheusServerReadyToServe()) { + break; + } + } + + uint16_t port = logger.getPrometheusPort(); + std::string response + = MakeHttpRequest("localhost", fmt::format("{}", port), "/metrics"); + EXPECT_THAT(response, HasSubstr("10101")); +} + } // namespace utl From f68ae2a0f4f45f4909b49da0e873c5ce1bcb35bc Mon Sep 17 00:00:00 2001 From: Ethan Mahintorabi Date: Thu, 20 Feb 2025 01:19:13 +0000 Subject: [PATCH 2/5] Fixes compile error with unique_ptr, and doc bug Signed-off-by: Ethan Mahintorabi --- src/utl/README.md | 52 ++++++++++++++++++------------------ src/utl/include/utl/Logger.h | 10 +++---- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/utl/README.md b/src/utl/README.md index ffb951c85e9..edf24977a58 100644 --- a/src/utl/README.md +++ b/src/utl/README.md @@ -9,6 +9,32 @@ The utility module contains the `man` command. - Parameters without square brackets `-param2 param2` are required. ``` +### Prometheus Metrics + +OpenROAD includes a metrics endpoint server that can track internal tool metrics over time. + +![page](/docs/images/grafana.png) + +To use this feature you need to do the following start the prometheus and grafana collectors + +[Detailed instructions](/etc/monitoring/README.md): +```shell +$ cd etc/monitoring +$ docker compose up -d +``` + +This will start a grafana endpoint ready to collect from the OpenROAD application you would +like to track. By default it's looking for an http server running on port 8080 on your localhost. + +To start the metrics endpoint in OpenROAD, run: +```tcl +utl::startPrometheusEndpoint 8080 +``` + +This is all configurable in the docker compose file, and you should be able to access grafana by going to +http://localhost:3000 username: admin, password: grafana. Go to the dashboard tab and click service, +then OpenROAD to see the pre-made dashboard. + ## Man installation The `man` command can be installed optionally as part of the OpenROAD @@ -95,32 +121,6 @@ Simply run the following script: ./test/regression ``` -## Prometheus Metrics - -OpenROAD includes a metrics endpoint server that can track internal tool metrics over time. - -![page](/docs/images/grafana.png) - -To use this feature you need to do the following start the prometheus and grafana collectors - -[Detailed instructions](/etc/monitoring/README.md): -```shell -$ cd etc/monitoring -$ docker compose up -d -``` - -This will start a grafana endpoint ready to collect from the OpenROAD application you would -like to track. By default it's looking for an http server running on port 8080 on your localhost. - -To start the metrics endpoint in OpenROAD, run: -```tcl -utl::startPrometheusEndpoint 8080 -``` - -This is all configurable in the docker compose file, and you should be able to access grafana by going to -http://localhost:3000 username: admin, password: grafana. Go to the dashboard tab and click service, -then OpenROAD to see the pre-made dashboard. - ## Limitations ## FAQs diff --git a/src/utl/include/utl/Logger.h b/src/utl/include/utl/Logger.h index 7be611c3eae..8d97afe7786 100644 --- a/src/utl/include/utl/Logger.h +++ b/src/utl/include/utl/Logger.h @@ -359,8 +359,8 @@ class Logger std::unique_ptr file_redirect_; // Prometheus server metrics collection - std::shared_ptr prometheus_registry_ = nullptr; - std::unique_ptr prometheus_metrics_ = nullptr; + std::shared_ptr prometheus_registry_; + std::unique_ptr prometheus_metrics_; // This matrix is pre-allocated so it can be safely updated // from multiple threads without locks. @@ -398,9 +398,9 @@ struct test_ostream { public: template - static auto test(int) - -> decltype(std::declval() << std::declval(), - std::true_type()); + static auto test(int) -> decltype(std::declval() + << std::declval(), + std::true_type()); template static auto test(...) -> std::false_type; From 16b92dea305092bde2c3fe96baaa7d6ec6349fad Mon Sep 17 00:00:00 2001 From: Ethan Mahintorabi Date: Thu, 20 Feb 2025 05:24:05 +0000 Subject: [PATCH 3/5] clang-tidy, and removing unused using statement Signed-off-by: Ethan Mahintorabi --- src/utl/include/utl/prometheus/benchmark.h | 13 ++-- .../include/utl/prometheus/ckms_quantiles.h | 8 +-- src/utl/include/utl/prometheus/counter.h | 7 ++- src/utl/include/utl/prometheus/family.h | 63 ++++++++++++------- src/utl/include/utl/prometheus/gauge.h | 4 +- src/utl/include/utl/prometheus/histogram.h | 9 ++- .../utl/prometheus/prometheus_metric.h | 2 +- src/utl/include/utl/prometheus/registry.h | 37 ++++++----- src/utl/include/utl/prometheus/summary.h | 10 ++- .../include/utl/prometheus/text_serializer.h | 10 ++- .../utl/prometheus/time_window_quantiles.h | 3 +- src/utl/src/prometheus/metrics_server.cpp | 1 - 12 files changed, 91 insertions(+), 76 deletions(-) diff --git a/src/utl/include/utl/prometheus/benchmark.h b/src/utl/include/utl/prometheus/benchmark.h index dda5c6c7644..465461a267a 100644 --- a/src/utl/include/utl/prometheus/benchmark.h +++ b/src/utl/include/utl/prometheus/benchmark.h @@ -23,7 +23,9 @@ #pragma once #include +#include +#include "client_metric.h" #include "family.h" #include "prometheus_metric.h" @@ -52,10 +54,10 @@ class Benchmark : public PrometheusMetric void start() { #ifndef NDEBUG - if (already_started) + if (already_started) { throw std::runtime_error("try to start already started counter"); - else - already_started = true; + } + already_started = true; #endif start_ = std::chrono::high_resolution_clock::now(); @@ -64,8 +66,9 @@ class Benchmark : public PrometheusMetric void stop() { #ifndef NDEBUG - if (already_started == false) + if (already_started == false) { throw std::runtime_error("try to stop already stoped counter"); + } #endif std::chrono::time_point stop; @@ -83,7 +86,7 @@ class Benchmark : public PrometheusMetric .count(); } - virtual ClientMetric Collect() const + ClientMetric Collect() const override { ClientMetric metric; metric.counter.value = Get(); diff --git a/src/utl/include/utl/prometheus/ckms_quantiles.h b/src/utl/include/utl/prometheus/ckms_quantiles.h index 57daf1f7e8b..4eb91e31177 100644 --- a/src/utl/include/utl/prometheus/ckms_quantiles.h +++ b/src/utl/include/utl/prometheus/ckms_quantiles.h @@ -68,7 +68,7 @@ class CKMSQuantiles public: explicit CKMSQuantiles(const std::vector& quantiles) - : quantiles_(quantiles), count_(0), buffer_{}, buffer_count_(0) + : quantiles_(quantiles) { } @@ -213,10 +213,10 @@ class CKMSQuantiles private: const std::reference_wrapper> quantiles_; - std::size_t count_; + std::size_t count_{0}; std::vector sample_; - std::array buffer_; - std::size_t buffer_count_; + std::array buffer_{}; + std::size_t buffer_count_{0}; }; } // namespace detail diff --git a/src/utl/include/utl/prometheus/counter.h b/src/utl/include/utl/prometheus/counter.h index a95c6fe1da7..3cbdec150df 100644 --- a/src/utl/include/utl/prometheus/counter.h +++ b/src/utl/include/utl/prometheus/counter.h @@ -72,16 +72,17 @@ class Counter : public PrometheusMetric void Increment(const Value& val) { ///< \brief Increment the counter by a given amount. The counter will not ///< change if the given amount is negative. - if (val > 0) + if (val > 0) { value += val; + } } - const Value Get() const + Value Get() const { ///< \brief Get the current value of the counter. return value; } - virtual ClientMetric Collect() const + ClientMetric Collect() const override { ///< /// \brief Get the current value of the counter. Collect is called by ///< the Registry when collecting metrics. ClientMetric metric; diff --git a/src/utl/include/utl/prometheus/family.h b/src/utl/include/utl/prometheus/family.h index e4dc06d994f..926fcd047a5 100644 --- a/src/utl/include/utl/prometheus/family.h +++ b/src/utl/include/utl/prometheus/family.h @@ -100,8 +100,9 @@ class Family : public Collectable static Hash hash_labels(const Labels& labels) { size_t seed = 0; - for (const Label& label : labels) + for (const Label& label : labels) { detail::hash_combine(&seed, label.first, label.second); + } return seed; } @@ -115,12 +116,15 @@ class Family : public Collectable bool nameStartsValid(const std::string& cur_name) { - if (cur_name.empty()) + if (cur_name.empty()) { return false; // must not be empty - if (isLocaleIndependentDigit(cur_name.front())) + } + if (isLocaleIndependentDigit(cur_name.front())) { return false; // must not start with a digit - if (cur_name.compare(0, 2, "__") == 0) + } + if (cur_name.compare(0, 2, "__") == 0) { return false; // must not start with "__" + } return true; } @@ -134,13 +138,15 @@ class Family : public Collectable /// \return true is valid, false otherwise bool CheckMetricName(const std::string& cur_name) { - if (!nameStartsValid(cur_name)) + if (!nameStartsValid(cur_name)) { return false; + } - for (const char& c : cur_name) - if (!isLocaleIndependentAlphaNumeric(c) && c != '_' && c != ':') + for (const char& c : cur_name) { + if (!isLocaleIndependentAlphaNumeric(c) && c != '_' && c != ':') { return false; - + } + } return true; } @@ -154,13 +160,15 @@ class Family : public Collectable /// \return true is valid, false otherwise bool CheckLabelName(const std::string& cur_name) { - if (!nameStartsValid(cur_name)) + if (!nameStartsValid(cur_name)) { return false; + } - for (const char& c : cur_name) - if (!isLocaleIndependentAlphaNumeric(c) && c != '_') + for (const char& c : cur_name) { + if (!isLocaleIndependentAlphaNumeric(c) && c != '_') { return false; - + } + } return true; } @@ -198,13 +206,15 @@ class Family : public Collectable const Labels& constant_labels_) : type(type_), name(name_), help(help_), constant_labels(constant_labels_) { - if (!CheckMetricName(name_)) + if (!CheckMetricName(name_)) { throw std::invalid_argument("Invalid metric name"); + } for (const Label& label_pair : constant_labels) { const std::string& label_name = label_pair.first; - if (!CheckLabelName(label_name)) + if (!CheckLabelName(label_name)) { throw std::invalid_argument("Invalid label name"); + } } } @@ -216,8 +226,9 @@ class Family : public Collectable { std::lock_guard lock{mutex}; - if (labels_reverse_lookup.count(metric) == 0) + if (labels_reverse_lookup.count(metric) == 0) { return; + } const Hash hash = labels_reverse_lookup.at(metric); metrics.erase(hash); @@ -254,8 +265,9 @@ class Family : public Collectable { std::lock_guard lock{mutex}; - if (metrics.empty()) + if (metrics.empty()) { return {}; + } MetricFamily family = MetricFamily{}; family.type = type; @@ -265,14 +277,15 @@ class Family : public Collectable for (const std::pair& metric_pair : metrics) { ClientMetric collected = metric_pair.second->Collect(); - for (const Label& constant_label : constant_labels) - collected.label.emplace_back( - ClientMetric::Label(constant_label.first, constant_label.second)); + for (const Label& constant_label : constant_labels) { + collected.label.emplace_back(constant_label.first, + constant_label.second); + } const Labels& metric_labels = labels.at(metric_pair.first); - for (const Label& metric_label : metric_labels) - collected.label.emplace_back( - ClientMetric::Label(metric_label.first, metric_label.second)); + for (const Label& metric_label : metric_labels) { + collected.label.emplace_back(metric_label.first, metric_label.second); + } family.metric.push_back(std::move(collected)); } @@ -333,11 +346,13 @@ class CustomFamily : public Family // check labels before create the new one for (const Label& label_pair : new_labels) { const std::string& label_name = label_pair.first; - if (!CheckLabelName(label_name)) + if (!CheckLabelName(label_name)) { throw std::invalid_argument("Invalid label name"); - if (constant_labels.count(label_name)) + } + if (constant_labels.count(label_name)) { throw std::invalid_argument( "Label name already present in constant labels"); + } } // create new one diff --git a/src/utl/include/utl/prometheus/gauge.h b/src/utl/include/utl/prometheus/gauge.h index d446d7fd6e7..774dcb0457b 100644 --- a/src/utl/include/utl/prometheus/gauge.h +++ b/src/utl/include/utl/prometheus/gauge.h @@ -87,12 +87,12 @@ class Gauge : public PrometheusMetric { value = val; } ///< \brief Set the gauge to the given value. - const Value Get() const + Value Get() const { return value; } ///< \brief Get the current value of the gauge. - virtual ClientMetric Collect() const + ClientMetric Collect() const override { ///< \brief Get the current value of the gauge. Collect is called by the ///< Registry when collecting metrics. ClientMetric metric; diff --git a/src/utl/include/utl/prometheus/histogram.h b/src/utl/include/utl/prometheus/histogram.h index cecf4ddb93b..7e7fdc3c3d0 100644 --- a/src/utl/include/utl/prometheus/histogram.h +++ b/src/utl/include/utl/prometheus/histogram.h @@ -73,8 +73,7 @@ class Histogram : public PrometheusMetric Histogram(const BucketBoundaries& buckets) : PrometheusMetric(static_type), bucket_boundaries_{buckets}, - bucket_counts_{buckets.size() + 1}, - sum_{} + bucket_counts_{buckets.size() + 1} { assert(std::is_sorted(std::begin(bucket_boundaries_), std::end(bucket_boundaries_))); @@ -123,7 +122,7 @@ class Histogram : public PrometheusMetric /// \brief Get the current value of the counter. /// /// Collect is called by the Registry when collecting metrics. - virtual ClientMetric Collect() const + ClientMetric Collect() const override { auto metric = ClientMetric{}; @@ -136,7 +135,7 @@ class Histogram : public PrometheusMetric bucket.upper_bound = i == bucket_boundaries_.size() ? std::numeric_limits::infinity() : static_cast(bucket_boundaries_[i]); - metric.histogram.bucket.push_back(std::move(bucket)); + metric.histogram.bucket.push_back(bucket); } metric.histogram.sample_count = cumulative_count; metric.histogram.sample_sum = static_cast(sum_.Get()); @@ -147,7 +146,7 @@ class Histogram : public PrometheusMetric private: const BucketBoundaries bucket_boundaries_; std::vector> bucket_counts_; - Gauge sum_; + Gauge sum_{}; }; /// \brief Return a builder to configure and register a Histogram metric. diff --git a/src/utl/include/utl/prometheus/prometheus_metric.h b/src/utl/include/utl/prometheus/prometheus_metric.h index cb904517d31..67dc3367e43 100644 --- a/src/utl/include/utl/prometheus/prometheus_metric.h +++ b/src/utl/include/utl/prometheus/prometheus_metric.h @@ -22,7 +22,7 @@ #pragma once -#include +#include #include "client_metric.h" diff --git a/src/utl/include/utl/prometheus/registry.h b/src/utl/include/utl/prometheus/registry.h index 5491646c889..5b36a1b9182 100644 --- a/src/utl/include/utl/prometheus/registry.h +++ b/src/utl/include/utl/prometheus/registry.h @@ -89,7 +89,7 @@ class Registry : public Collectable /// function. /// /// \return Zero or more metrics and their samples. - virtual MetricFamilies Collect() const + MetricFamilies Collect() const override { std::lock_guard lock{mutex}; @@ -116,31 +116,34 @@ class Registry : public Collectable for (const FamilyPtr& family_ptr : families) { if (family_ptr->GetName() == name) { if (family_ptr->type - != CustomFamily::static_type) // found family with this name and - // with different type + != CustomFamily::static_type) { // found family with this name and + // with different type throw std::invalid_argument( "Family name already exists with different type"); + } - else { // found family with this name and the same type - switch (insert_behavior) { - case InsertBehavior::Throw: - throw std::invalid_argument("Family name already exists"); - case InsertBehavior::Merge: - if (family_ptr->GetConstantLabels() == labels) - return dynamic_cast(*family_ptr); - else // this strange rule was in previos version prometheus cpp - found_one_but_not_merge = true; - case InsertBehavior::NonStandardAppend: - continue; - } + // found family with this name and the same type + switch (insert_behavior) { + case InsertBehavior::Throw: + throw std::invalid_argument("Family name already exists"); + case InsertBehavior::Merge: + if (family_ptr->GetConstantLabels() == labels) { + return dynamic_cast(*family_ptr); + } else { // this strange rule was in previous version prometheus + // cpp + found_one_but_not_merge = true; + } + case InsertBehavior::NonStandardAppend: + continue; } } } - if (found_one_but_not_merge) // this strange rule was in previos version - // prometheus cpp + if (found_one_but_not_merge) { // this strange rule was in previous version + // prometheus cpp throw std::invalid_argument( "Family name already exists with different labels"); + } std::unique_ptr new_family_ptr( new CustomFamily(name, help, labels)); diff --git a/src/utl/include/utl/prometheus/summary.h b/src/utl/include/utl/prometheus/summary.h index dfff6f517bf..5c3ee778ee1 100644 --- a/src/utl/include/utl/prometheus/summary.h +++ b/src/utl/include/utl/prometheus/summary.h @@ -72,8 +72,8 @@ class Summary : PrometheusMetric const Quantiles quantiles_; mutable std::mutex mutex_; - std::uint64_t count_; - double sum_; + std::uint64_t count_{0}; + double sum_{0}; detail::TimeWindowQuantiles quantile_values_; public: @@ -109,8 +109,6 @@ class Summary : PrometheusMetric int age_buckets = 5) : PrometheusMetric(static_type), quantiles_{quantiles}, - count_{0}, - sum_{0}, quantile_values_(quantiles_, max_age, age_buckets) { } @@ -128,7 +126,7 @@ class Summary : PrometheusMetric /// \brief Get the current value of the summary. /// /// Collect is called by the Registry when collecting metrics. - virtual ClientMetric Collect() const + ClientMetric Collect() const override { auto metric = ClientMetric{}; @@ -139,7 +137,7 @@ class Summary : PrometheusMetric auto metricQuantile = ClientMetric::Quantile{}; metricQuantile.quantile = quantile.quantile; metricQuantile.value = quantile_values_.get(quantile.quantile); - metric.summary.quantile.push_back(std::move(metricQuantile)); + metric.summary.quantile.push_back(metricQuantile); } metric.summary.sample_count = count_; metric.summary.sample_sum = sum_; diff --git a/src/utl/include/utl/prometheus/text_serializer.h b/src/utl/include/utl/prometheus/text_serializer.h index b0391da00cd..c952193f15e 100644 --- a/src/utl/include/utl/prometheus/text_serializer.h +++ b/src/utl/include/utl/prometheus/text_serializer.h @@ -22,9 +22,8 @@ #pragma once -#include - #include +#include #include #include #include @@ -42,12 +41,11 @@ class TextSerializer // Write a double as a string, with proper formatting for infinity and NaN static void WriteValue(std::ostream& out, double value) { - if (std::isnan(value)) + if (std::isnan(value)) { out << "Nan"; - else if (std::isinf(value)) + } else if (std::isinf(value)) { out << (value < 0 ? "-Inf" : "+Inf"); - - else { + } else { std::array buffer; #if __cpp_lib_to_chars >= 201611L diff --git a/src/utl/include/utl/prometheus/time_window_quantiles.h b/src/utl/include/utl/prometheus/time_window_quantiles.h index 25f1d17d5ee..a87dada7830 100644 --- a/src/utl/include/utl/prometheus/time_window_quantiles.h +++ b/src/utl/include/utl/prometheus/time_window_quantiles.h @@ -41,7 +41,6 @@ class TimeWindowQuantiles const int age_buckets) : quantiles_(quantiles), ckms_quantiles_(age_buckets, CKMSQuantiles(quantiles_)), - current_bucket_(0), last_rotation_(Clock::now()), rotation_interval_(max_age / age_buckets) { @@ -80,7 +79,7 @@ class TimeWindowQuantiles const std::vector& quantiles_; mutable std::vector ckms_quantiles_; - mutable std::size_t current_bucket_; + mutable std::size_t current_bucket_{0}; mutable Clock::time_point last_rotation_; const Clock::duration rotation_interval_; diff --git a/src/utl/src/prometheus/metrics_server.cpp b/src/utl/src/prometheus/metrics_server.cpp index bc89f56e825..d874e2848bc 100644 --- a/src/utl/src/prometheus/metrics_server.cpp +++ b/src/utl/src/prometheus/metrics_server.cpp @@ -22,7 +22,6 @@ namespace utl { PrometheusMetricsServer::~PrometheusMetricsServer() { - using tcp = boost::asio::ip::tcp; shutdown_ = true; // Make a dummy connection to unblock the accept(). From ba255c4155b3f77a3681adc1ed88ec6d16b01b38 Mon Sep 17 00:00:00 2001 From: Ethan Mahintorabi Date: Mon, 24 Feb 2025 23:06:43 +0000 Subject: [PATCH 4/5] addressing PR feedback Signed-off-by: Ethan Mahintorabi --- src/gpl/src/nesterovPlace.cpp | 2 +- src/utl/include/utl/Logger.h | 6 +-- src/utl/include/utl/prometheus/builder.h | 2 +- src/utl/include/utl/prometheus/collectable.h | 2 +- src/utl/include/utl/prometheus/counter.h | 6 +-- src/utl/include/utl/prometheus/family.h | 10 ++-- src/utl/include/utl/prometheus/gauge.h | 6 +-- src/utl/include/utl/prometheus/histogram.h | 6 +-- .../include/utl/prometheus/metrics_server.h | 27 +++++----- src/utl/include/utl/prometheus/registry.h | 4 +- src/utl/include/utl/prometheus/save_to_file.h | 6 +-- src/utl/include/utl/prometheus/summary.h | 6 +-- src/utl/src/Logger.cpp | 4 +- src/utl/src/prometheus/metrics_server.cpp | 49 ++++++++++--------- src/utl/test/cpp/TestCFileUtils.cpp | 2 +- 15 files changed, 71 insertions(+), 67 deletions(-) diff --git a/src/gpl/src/nesterovPlace.cpp b/src/gpl/src/nesterovPlace.cpp index 5a831af1585..9ffb6a98607 100644 --- a/src/gpl/src/nesterovPlace.cpp +++ b/src/gpl/src/nesterovPlace.cpp @@ -227,7 +227,7 @@ void NesterovPlace::init() totalBaseWireLengthCoeff += nb->getBaseWireLengthCoef(); } - std::shared_ptr registry = log_->getRegistry(); + std::shared_ptr registry = log_->getRegistry(); auto& hpwl_gauge_family = utl::BuildGauge() .Name("ord_hpwl") diff --git a/src/utl/include/utl/Logger.h b/src/utl/include/utl/Logger.h index 8d97afe7786..9df4e4ed3c1 100644 --- a/src/utl/include/utl/Logger.h +++ b/src/utl/include/utl/Logger.h @@ -60,7 +60,7 @@ namespace utl { class PrometheusMetricsServer; -class Registry; +class PrometheusRegistry; // Keep this sorted #define FOREACH_TOOL(X) \ @@ -238,7 +238,7 @@ class Logger } void startPrometheusEndpoint(uint16_t port); - std::shared_ptr getRegistry(); + std::shared_ptr getRegistry(); bool isPrometheusServerReadyToServe(); uint16_t getPrometheusPort(); @@ -359,7 +359,7 @@ class Logger std::unique_ptr file_redirect_; // Prometheus server metrics collection - std::shared_ptr prometheus_registry_; + std::shared_ptr prometheus_registry_; std::unique_ptr prometheus_metrics_; // This matrix is pre-allocated so it can be safely updated diff --git a/src/utl/include/utl/prometheus/builder.h b/src/utl/include/utl/prometheus/builder.h index ddc6d4f3b80..0ffffb1b7fd 100644 --- a/src/utl/include/utl/prometheus/builder.h +++ b/src/utl/include/utl/prometheus/builder.h @@ -52,7 +52,7 @@ class Builder help_ = help; return *this; } - CustomFamily& Register(Registry& registry) + CustomFamily& Register(PrometheusRegistry& registry) { return registry.Add>(name_, help_, labels_); } diff --git a/src/utl/include/utl/prometheus/collectable.h b/src/utl/include/utl/prometheus/collectable.h index 3291986fd75..d75ebb5f46b 100644 --- a/src/utl/include/utl/prometheus/collectable.h +++ b/src/utl/include/utl/prometheus/collectable.h @@ -31,7 +31,7 @@ namespace utl { /// @brief Interface implemented by anything that can be used by Prometheus to /// collect metrics. /// -/// A Collectable has to be registered for collection. See Registry. +/// A Collectable has to be registered for collection. See PrometheusRegistry. class Collectable { public: diff --git a/src/utl/include/utl/prometheus/counter.h b/src/utl/include/utl/prometheus/counter.h index 3cbdec150df..b73fb3a9fde 100644 --- a/src/utl/include/utl/prometheus/counter.h +++ b/src/utl/include/utl/prometheus/counter.h @@ -84,7 +84,7 @@ class Counter : public PrometheusMetric ClientMetric Collect() const override { ///< /// \brief Get the current value of the counter. Collect is called by - ///< the Registry when collecting metrics. + ///< the PrometheusRegistry when collecting metrics. ClientMetric metric; metric.counter.value = static_cast(value); return metric; @@ -118,7 +118,7 @@ class Counter : public PrometheusMetric /// Example usage: /// /// \code -/// auto registry = std::make_shared(); +/// auto registry = std::make_shared(); /// auto& counter_family = utl::BuildCounter() /// .Name("some_name") /// .Help("Additional description.") @@ -137,7 +137,7 @@ class Counter : public PrometheusMetric /// key-value pairs (= labels) to the metric. /// /// To finish the configuration of the Counter metric, register it with -/// Register(Registry&). +/// Register(PrometheusRegistry&). using BuildCounter = Builder>; } // namespace utl diff --git a/src/utl/include/utl/prometheus/family.h b/src/utl/include/utl/prometheus/family.h index 926fcd047a5..e2747c516a9 100644 --- a/src/utl/include/utl/prometheus/family.h +++ b/src/utl/include/utl/prometheus/family.h @@ -258,7 +258,7 @@ class Family : public Collectable /// \brief Returns the current value of each dimensional data. /// - /// Collect is called by the Registry when collecting metrics. + /// Collect is called by the PrometheusRegistry when collecting metrics. /// /// \return Zero or more samples for each dimensional data. MetricFamilies Collect() const override @@ -376,7 +376,7 @@ class CustomFamily : public Family /// Example usage: /// /// \code - /// auto registry = std::make_shared(); + /// auto registry = std::make_shared(); /// auto& counter_family = utl::Counter_family::build("some_name", /// "Additional description.", {{"key", "value"}}, *registry); /// @@ -392,9 +392,9 @@ class CustomFamily : public Family /// key-value pairs (= labels) to the metric. /// /// To finish the configuration of the Counter metric, register it with - /// Register(Registry&). - template - static CustomFamily& Build(Registry& registry, + /// Register(PrometheusRegistry&). + template + static CustomFamily& Build(PrometheusRegistry& registry, const std::string& name, const std::string& help, const Family::Labels& labels = Family::Labels()) diff --git a/src/utl/include/utl/prometheus/gauge.h b/src/utl/include/utl/prometheus/gauge.h index 774dcb0457b..fa7cc33f085 100644 --- a/src/utl/include/utl/prometheus/gauge.h +++ b/src/utl/include/utl/prometheus/gauge.h @@ -94,7 +94,7 @@ class Gauge : public PrometheusMetric ClientMetric Collect() const override { ///< \brief Get the current value of the gauge. Collect is called by the - ///< Registry when collecting metrics. + ///< PrometheusRegistry when collecting metrics. ClientMetric metric; metric.gauge.value = static_cast(value); return metric; @@ -146,7 +146,7 @@ class Gauge : public PrometheusMetric /// Example usage: /// /// \code -/// auto registry = std::make_shared(); +/// auto registry = std::make_shared(); /// auto& gauge_family = utl::BuildGauge() /// .Name("some_name") /// .Help("Additional description.") @@ -165,7 +165,7 @@ class Gauge : public PrometheusMetric /// key-value pairs (= labels) to the metric. /// /// To finish the configuration of the Gauge metric register it with -/// Register(Registry&). +/// Register(PrometheusRegistry&). using BuildGauge = Builder>; } // namespace utl diff --git a/src/utl/include/utl/prometheus/histogram.h b/src/utl/include/utl/prometheus/histogram.h index 7e7fdc3c3d0..cb9342ee049 100644 --- a/src/utl/include/utl/prometheus/histogram.h +++ b/src/utl/include/utl/prometheus/histogram.h @@ -121,7 +121,7 @@ class Histogram : public PrometheusMetric /// \brief Get the current value of the counter. /// - /// Collect is called by the Registry when collecting metrics. + /// Collect is called by the PrometheusRegistry when collecting metrics. ClientMetric Collect() const override { auto metric = ClientMetric{}; @@ -156,7 +156,7 @@ class Histogram : public PrometheusMetric /// Example usage: /// /// \code -/// auto registry = std::make_shared(); +/// auto registry = std::make_shared(); /// auto& histogram_family = utl::BuildHistogram() /// .Name("some_name") /// .Help("Additional description.") @@ -175,7 +175,7 @@ class Histogram : public PrometheusMetric /// key-value pairs (= labels) to the metric. /// /// To finish the configuration of the Histogram metric register it with -/// Register(Registry&). +/// Register(PrometheusRegistry&). using BuildHistogram = Builder>; } // namespace utl diff --git a/src/utl/include/utl/prometheus/metrics_server.h b/src/utl/include/utl/prometheus/metrics_server.h index 06a0fa9d57d..6bc20ca16a6 100644 --- a/src/utl/include/utl/prometheus/metrics_server.h +++ b/src/utl/include/utl/prometheus/metrics_server.h @@ -24,19 +24,8 @@ class Logger; namespace utl { class PrometheusMetricsServer { - std::thread worker_thread_; - std::shared_ptr registry_ptr_{nullptr}; - uint16_t port_; - std::atomic logger_; - bool shutdown_ = false; - bool is_ready_ = false; - - void RunServer(); - void WorkerFunction(); - public: - ~PrometheusMetricsServer(); - PrometheusMetricsServer(std::shared_ptr& registry_, + PrometheusMetricsServer(std::shared_ptr& registry_, utl::Logger* logger, uint16_t port) { @@ -46,13 +35,25 @@ class PrometheusMetricsServer worker_thread_ = std::thread(&PrometheusMetricsServer::WorkerFunction, this); } + ~PrometheusMetricsServer(); bool is_ready() { return is_ready_; } uint16_t port() { return port_; } - void SetRegistry(std::shared_ptr& new_registry_ptr) + void SetRegistry(std::shared_ptr& new_registry_ptr) { registry_ptr_ = new_registry_ptr; } + + private: + std::thread worker_thread_; + std::shared_ptr registry_ptr_{nullptr}; + uint16_t port_; + std::atomic logger_; + bool shutdown_ = false; + bool is_ready_ = false; + + void RunServer(); + void WorkerFunction(); }; } // namespace utl diff --git a/src/utl/include/utl/prometheus/registry.h b/src/utl/include/utl/prometheus/registry.h index 5b36a1b9182..e6e076984e7 100644 --- a/src/utl/include/utl/prometheus/registry.h +++ b/src/utl/include/utl/prometheus/registry.h @@ -47,7 +47,7 @@ namespace utl { /// /// The class is thread-safe. No concurrent call to any API of this type causes /// a data race. -class Registry : public Collectable +class PrometheusRegistry : public Collectable { public: /// \brief How to deal with repeatedly added family names for a type. @@ -78,7 +78,7 @@ class Registry : public Collectable /// \brief name Create a new registry. /// /// \param insert_behavior How to handle families with the same name. - Registry(InsertBehavior insert_behavior_ = InsertBehavior::Merge) + PrometheusRegistry(InsertBehavior insert_behavior_ = InsertBehavior::Merge) : insert_behavior(insert_behavior_) { } diff --git a/src/utl/include/utl/prometheus/save_to_file.h b/src/utl/include/utl/prometheus/save_to_file.h index c80554d1238..1fbbe868f98 100644 --- a/src/utl/include/utl/prometheus/save_to_file.h +++ b/src/utl/include/utl/prometheus/save_to_file.h @@ -37,7 +37,7 @@ class SaveToFile std::chrono::seconds period{1}; std::string filename; std::thread worker_thread; - std::shared_ptr registry_ptr{nullptr}; + std::shared_ptr registry_ptr{nullptr}; bool must_die{false}; void save_data() @@ -82,7 +82,7 @@ class SaveToFile worker_thread.join(); } - SaveToFile(std::shared_ptr& registry_, + SaveToFile(std::shared_ptr& registry_, const std::chrono::seconds& period_, const std::string& filename_) { @@ -106,7 +106,7 @@ class SaveToFile return open_success; } - void set_registry(std::shared_ptr& new_registry_ptr) + void set_registry(std::shared_ptr& new_registry_ptr) { registry_ptr = new_registry_ptr; } diff --git a/src/utl/include/utl/prometheus/summary.h b/src/utl/include/utl/prometheus/summary.h index 5c3ee778ee1..b3741ce5cd5 100644 --- a/src/utl/include/utl/prometheus/summary.h +++ b/src/utl/include/utl/prometheus/summary.h @@ -125,7 +125,7 @@ class Summary : PrometheusMetric /// \brief Get the current value of the summary. /// - /// Collect is called by the Registry when collecting metrics. + /// Collect is called by the PrometheusRegistry when collecting metrics. ClientMetric Collect() const override { auto metric = ClientMetric{}; @@ -153,7 +153,7 @@ class Summary : PrometheusMetric /// Example usage: /// /// \code -/// auto registry = std::make_shared(); +/// auto registry = std::make_shared(); /// auto& summary_family = utl::BuildSummary() /// .Name("some_name") /// .Help("Additional description.") @@ -172,7 +172,7 @@ class Summary : PrometheusMetric /// key-value pairs (= labels) to the metric. /// /// To finish the configuration of the Summary metric register it with -/// Register(Registry&). +/// Register(PrometheusRegistry&). using BuildSummary = Builder; } // namespace utl diff --git a/src/utl/src/Logger.cpp b/src/utl/src/Logger.cpp index 434b54001a2..64c45306c61 100644 --- a/src/utl/src/Logger.cpp +++ b/src/utl/src/Logger.cpp @@ -72,7 +72,7 @@ Logger::Logger(const char* log_filename, const char* metrics_filename) } } - prometheus_registry_ = std::make_shared(); + prometheus_registry_ = std::make_shared(); } Logger::~Logger() @@ -334,7 +334,7 @@ void Logger::startPrometheusEndpoint(uint16_t port) prometheus_registry_, this, port); } -std::shared_ptr Logger::getRegistry() +std::shared_ptr Logger::getRegistry() { return prometheus_registry_; } diff --git a/src/utl/src/prometheus/metrics_server.cpp b/src/utl/src/prometheus/metrics_server.cpp index d874e2848bc..a83143c1717 100644 --- a/src/utl/src/prometheus/metrics_server.cpp +++ b/src/utl/src/prometheus/metrics_server.cpp @@ -18,32 +18,13 @@ #include "utl/Logger.h" -namespace utl { +namespace { -PrometheusMetricsServer::~PrometheusMetricsServer() -{ - shutdown_ = true; - - // Make a dummy connection to unblock the accept(). - if (is_ready_) { // Only connect if the server was actually started. - try { - boost::asio::io_context - io_context; // Use a separate io_context for the connection. - boost::asio::ip::tcp::socket socket(io_context); - boost::asio::ip::tcp::endpoint endpoint( - boost::asio::ip::address::from_string("127.0.0.1"), port_); - socket.connect(endpoint); // This will unblock the accept(). - } catch (const std::exception& e) { /*Do nothing, we're dying*/ - } - } - worker_thread_.join(); -} - -std::string SnapshotPrometheusMetrics(Registry* registry) +std::string SnapshotPrometheusMetrics(utl::PrometheusRegistry* registry) { if (registry) { std::stringstream stringstream; - TextSerializer::Serialize(stringstream, registry->Collect()); + utl::TextSerializer::Serialize(stringstream, registry->Collect()); return stringstream.str(); } @@ -53,7 +34,7 @@ std::string SnapshotPrometheusMetrics(Registry* registry) boost::beast::http::response HandleRequest( boost::beast::http::request& request, boost::asio::ip::tcp::socket& socket, - Registry* registry) + utl::PrometheusRegistry* registry) { namespace http = boost::beast::http; @@ -74,6 +55,28 @@ boost::beast::http::response HandleRequest( return response; } +} // namespace + +namespace utl { + +PrometheusMetricsServer::~PrometheusMetricsServer() +{ + shutdown_ = true; + + // Make a dummy connection to unblock the accept(). + if (is_ready_) { // Only connect if the server was actually started. + try { + boost::asio::io_context + io_context; // Use a separate io_context for the connection. + boost::asio::ip::tcp::socket socket(io_context); + boost::asio::ip::tcp::endpoint endpoint( + boost::asio::ip::address::from_string("127.0.0.1"), port_); + socket.connect(endpoint); // This will unblock the accept(). + } catch (const std::exception& e) { /*Do nothing, we're dying*/ + } + } + worker_thread_.join(); +} void PrometheusMetricsServer::RunServer() { diff --git a/src/utl/test/cpp/TestCFileUtils.cpp b/src/utl/test/cpp/TestCFileUtils.cpp index e5e093fb1fc..09333e8a80a 100644 --- a/src/utl/test/cpp/TestCFileUtils.cpp +++ b/src/utl/test/cpp/TestCFileUtils.cpp @@ -251,7 +251,7 @@ TEST(Utl, metrics_server_responds_with_basic_metric) { Logger logger; logger.startPrometheusEndpoint(0); - std::shared_ptr registry = logger.getRegistry(); + std::shared_ptr registry = logger.getRegistry(); auto& test_gauge_family = BuildGauge() .Name("test_gauge") .Help("A test gauge for testing") From 90c351973f1bf417bb3fc550966020a46510e00f Mon Sep 17 00:00:00 2001 From: Ethan Mahintorabi Date: Mon, 24 Feb 2025 23:09:53 +0000 Subject: [PATCH 5/5] fix markdown formatting Signed-off-by: Ethan Mahintorabi --- src/utl/README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/utl/README.md b/src/utl/README.md index edf24977a58..904f2cbc19e 100644 --- a/src/utl/README.md +++ b/src/utl/README.md @@ -62,10 +62,10 @@ man #### Options -| Switch Name | Description | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `name` | Name of the command/message to query. | -| `-manpath` | Include optional path to man pages (e.g. ~/OpenROAD/docs/cat). | +| Switch Name | Description | +| ----- | ----- | +| `name` | Name of the command/message to query. | +| `-manpath` | Include optional path to man pages (e.g. ~/OpenROAD/docs/cat). | | `-no_pager` | This flag determines whether you wish to see all of the man output at once. Default value is `False`, which shows a buffered output. | ## Example scripts @@ -90,13 +90,13 @@ tee (-file filename | -variable name) #### Options -| Switch Name | Description | -| ---------------- | ----------------------------------- | -| `-file filename` | File to redirect output into. | -| `-variable name` | Direct output into a variable. | -| `-append` | Append to file. | -| `-quiet` | Do not send output to standard out. | -| `command` | Command to execute. | +| Switch Name | Description | +| ----- | ----- | +| `-file filename` | File to redirect output into. | +| `-variable name` | Direct output into a variable. | +| `-append` | Append to file. | +| `-quiet` | Do not send output to standard out. | +| `command` | Command to execute. | ## Example scripts