From 5f3f3bd9839b0d4e185a13de05fc6b9a24e6067f Mon Sep 17 00:00:00 2001 From: liyuqian Date: Fri, 8 Feb 2019 14:47:22 -0800 Subject: [PATCH] Add mock capability to PerformanceOverlayLayer (#7537) So we can do golden test on PerformanceOverlay to avoid regression like https://github.com/flutter/flutter/issues/26387 --- ci/licenses_golden/licenses_flutter | 1 + flow/BUILD.gn | 1 + flow/layers/performance_overlay_layer.cc | 31 ++++-- flow/layers/performance_overlay_layer.h | 6 +- .../performance_overlay_layer_unittests.cc | 90 ++++++++++++++++++ .../resources/performance_overlay_gold.png | Bin 0 -> 16572 bytes 6 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 flow/layers/performance_overlay_layer_unittests.cc create mode 100644 testing/resources/performance_overlay_gold.png diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 7d5e643fd..35ea7cad1 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -57,6 +57,7 @@ FILE: ../../../flutter/flow/layers/opacity_layer.cc FILE: ../../../flutter/flow/layers/opacity_layer.h FILE: ../../../flutter/flow/layers/performance_overlay_layer.cc FILE: ../../../flutter/flow/layers/performance_overlay_layer.h +FILE: ../../../flutter/flow/layers/performance_overlay_layer_unittests.cc FILE: ../../../flutter/flow/layers/physical_shape_layer.cc FILE: ../../../flutter/flow/layers/physical_shape_layer.h FILE: ../../../flutter/flow/layers/picture_layer.cc diff --git a/flow/BUILD.gn b/flow/BUILD.gn index 6b345aa87..003fb326b 100644 --- a/flow/BUILD.gn +++ b/flow/BUILD.gn @@ -95,6 +95,7 @@ executable("flow_unittests") { sources = [ "matrix_decomposition_unittests.cc", "raster_cache_unittests.cc", + "layers/performance_overlay_layer_unittests.cc", ] deps = [ diff --git a/flow/layers/performance_overlay_layer.cc b/flow/layers/performance_overlay_layer.cc index c80786e86..e7f40058c 100644 --- a/flow/layers/performance_overlay_layer.cc +++ b/flow/layers/performance_overlay_layer.cc @@ -15,8 +15,12 @@ namespace { void DrawStatisticsText(SkCanvas& canvas, const std::string& string, int x, - int y) { + int y, + const std::string& font_path) { SkFont font; + if (font_path != "") { + font = SkFont(SkTypeface::MakeFromFile(font_path.c_str())); + } font.setSize(15); font.setLinearMetrics(false); SkPaint paint; @@ -33,7 +37,8 @@ void VisualizeStopWatch(SkCanvas& canvas, SkScalar height, bool show_graph, bool show_labels, - const std::string& label_prefix) { + const std::string& label_prefix, + const std::string& font_path) { const int label_x = 8; // distance from x const int label_y = -10; // distance from y+height @@ -51,14 +56,20 @@ void VisualizeStopWatch(SkCanvas& canvas, stream << label_prefix << " " << "max " << max_ms_per_frame << " ms/frame, " << "avg " << average_ms_per_frame << " ms/frame"; - DrawStatisticsText(canvas, stream.str(), x + label_x, y + height + label_y); + DrawStatisticsText(canvas, stream.str(), x + label_x, y + height + label_y, + font_path); } } } // namespace -PerformanceOverlayLayer::PerformanceOverlayLayer(uint64_t options) - : options_(options) {} +PerformanceOverlayLayer::PerformanceOverlayLayer(uint64_t options, + const char* font_path) + : options_(options) { + if (font_path != nullptr) { + font_path_ = font_path; + } +} void PerformanceOverlayLayer::Paint(PaintContext& context) const { const int padding = 8; @@ -73,15 +84,15 @@ void PerformanceOverlayLayer::Paint(PaintContext& context) const { SkScalar height = paint_bounds().height() / 2; SkAutoCanvasRestore save(context.leaf_nodes_canvas, true); - VisualizeStopWatch(*context.leaf_nodes_canvas, context.frame_time, x, y, - width, height - padding, - options_ & kVisualizeRasterizerStatistics, - options_ & kDisplayRasterizerStatistics, "GPU"); + VisualizeStopWatch( + *context.leaf_nodes_canvas, context.frame_time, x, y, width, + height - padding, options_ & kVisualizeRasterizerStatistics, + options_ & kDisplayRasterizerStatistics, "GPU", font_path_); VisualizeStopWatch(*context.leaf_nodes_canvas, context.engine_time, x, y + height, width, height - padding, options_ & kVisualizeEngineStatistics, - options_ & kDisplayEngineStatistics, "UI"); + options_ & kDisplayEngineStatistics, "UI", font_path_); } } // namespace flow diff --git a/flow/layers/performance_overlay_layer.h b/flow/layers/performance_overlay_layer.h index b5f20ecbd..a47b836c4 100644 --- a/flow/layers/performance_overlay_layer.h +++ b/flow/layers/performance_overlay_layer.h @@ -5,6 +5,8 @@ #ifndef FLUTTER_FLOW_LAYERS_PERFORMANCE_OVERLAY_LAYER_H_ #define FLUTTER_FLOW_LAYERS_PERFORMANCE_OVERLAY_LAYER_H_ +#include + #include "flutter/flow/layers/layer.h" #include "flutter/fml/macros.h" @@ -17,12 +19,14 @@ const int kVisualizeEngineStatistics = 1 << 3; class PerformanceOverlayLayer : public Layer { public: - explicit PerformanceOverlayLayer(uint64_t options); + explicit PerformanceOverlayLayer(uint64_t options, + const char* font_path = nullptr); void Paint(PaintContext& context) const override; private: int options_; + std::string font_path_; FML_DISALLOW_COPY_AND_ASSIGN(PerformanceOverlayLayer); }; diff --git a/flow/layers/performance_overlay_layer_unittests.cc b/flow/layers/performance_overlay_layer_unittests.cc new file mode 100644 index 000000000..21eb30653 --- /dev/null +++ b/flow/layers/performance_overlay_layer_unittests.cc @@ -0,0 +1,90 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/flow/layers/performance_overlay_layer.h" +#include "flutter/flow/raster_cache.h" + +#include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/utils/SkBase64.h" + +#include "gtest/gtest.h" + +// To get the size of kMockedTimes in compile time. +template +constexpr int size(const T (&array)[N]) noexcept { + return N; +} + +constexpr int kMockedTimes[] = {17, 1, 4, 24, 4, 25, 30, 4, 13, 34, + 14, 0, 18, 9, 32, 36, 26, 23, 5, 8, + 32, 18, 29, 16, 29, 18, 0, 36, 33, 10}; + +const char* kGoldenFileName = + "flutter/testing/resources/performance_overlay_gold.png"; + +const char* kNewGoldenFileName = + "flutter/testing/resources/performance_overlay_gold_new.png"; + +// Ensure the same font across different operation systems. +const char* kFontFilePath = + "flutter/third_party/txt/third_party/fonts/Roboto-Regular.ttf"; + +TEST(PerformanceOverlayLayer, Gold) { + flow::Stopwatch mock_stopwatch; + for (int i = 0; i < size(kMockedTimes); ++i) { + mock_stopwatch.SetLapTime( + fml::TimeDelta::FromMilliseconds(kMockedTimes[i])); + } + + const SkImageInfo image_info = SkImageInfo::MakeN32Premul(1000, 1000); + sk_sp surface = SkSurface::MakeRaster(image_info); + + ASSERT_TRUE(surface != nullptr); + + flow::TextureRegistry unused_texture_registry; + + flow::Layer::PaintContext paintContext = { + nullptr, surface->getCanvas(), nullptr, mock_stopwatch, + mock_stopwatch, unused_texture_registry, nullptr, false}; + + flow::PerformanceOverlayLayer layer(flow::kDisplayRasterizerStatistics | + flow::kVisualizeRasterizerStatistics | + flow::kDisplayEngineStatistics | + flow::kVisualizeEngineStatistics, + kFontFilePath); + layer.set_paint_bounds(SkRect::MakeWH(1000, 400)); + surface->getCanvas()->clear(SK_ColorTRANSPARENT); + layer.Paint(paintContext); + + sk_sp snapshot = surface->makeImageSnapshot(); + sk_sp snapshot_data = snapshot->encodeToData(); + + sk_sp golden_data = SkData::MakeFromFileName(kGoldenFileName); + EXPECT_TRUE(golden_data != nullptr) + << "Golden file not found: " << kGoldenFileName << ".\n" + << "Please make sure that the unit test is run from the right directory " + << "(e.g., flutter/engine/src)"; + + const bool golden_data_matches = golden_data->equals(snapshot_data.get()); + if (!golden_data_matches) { + SkFILEWStream wstream(kNewGoldenFileName); + wstream.write(snapshot_data->data(), snapshot_data->size()); + wstream.flush(); + + size_t b64_size = + SkBase64::Encode(snapshot_data->data(), snapshot_data->size(), nullptr); + char* b64_data = new char[b64_size]; + SkBase64::Encode(snapshot_data->data(), snapshot_data->size(), b64_data); + + EXPECT_TRUE(golden_data_matches) + << "Golden file mismatch. Please check " + << "the difference between " << kGoldenFileName << " and " + << kNewGoldenFileName << ", and replace the former " + << "with the latter if the difference looks good.\n\n" + << "See also the base64 encoded " << kNewGoldenFileName << ":\n" + << b64_data; + + delete[] b64_data; + } +} diff --git a/testing/resources/performance_overlay_gold.png b/testing/resources/performance_overlay_gold.png new file mode 100644 index 0000000000000000000000000000000000000000..119551f705793600890950be96b6b0d60688cc54 GIT binary patch literal 16572 zcmeI4c~n!`mjAIV3ra+AKuQrPN(B`KCqS7}f{4nbGLKOiWeN}=Od*ys2ndu?fJ{L_ zWD*dNAwY;S$`}~~1QHp;lmrMQB#`hssp{_4@As--zt#PIfBe>}yOt|@FWh_XIeYK3 z_xH17^lc+Ok=;jj3kV2^+`Mt^u7JSKIsPA^@4y+uoPlZZ@hx2Yrl}A(LWLZk3JClt zaP!(#)4+_S@gQ$AN~Zi4Wx?Lg)Aj297sJiYOr)UXq_{fiLEFQ;i&NEP8jEUxz@-Ki zk<$v&AhV_Oh3p*7K|AGnzuJD2L<5(O)Wh;dlf`jT#K4_TM)v&hbcg;s?Qgypb0v(E zAg)&)#f9(dJkJX)JSoA-2I1kA%d&fPek?&9{Ew+*5%aQsCpKpZ~h_Kc9_9&E1NRloHH} z8j(*tp*XF2pBy)KVZVfcfcN70$9utz_x<>4r+~oE-wFZK6cF3-jT-p4_UO@nz7*Is zDl%DS2`=nu75dND#U$OL+7sogNl&%C{Lh{}k^986v}kdm=1!$;vCYI0FYzX7?0>Qj zw*|2Dilt>`J_GlNQ5jFPg8iB>pQ_T;kxpI_SLUi4mB&s;F2u?zw|*xwCn6$(J2JGg zL*NnqY0Gyq#}KjVe=@SYYFS5^^|M0fjHLwxHp(yjv#<1TZ;O5+U9!X`qq9T=1U{}i ze)}f|*L5{Ye@~^{r`wDB_OQ6%$1D=}{qN25N1qDeuR~>SW;L*Uj zvLLpu3U1n*-;&{A8-*-(7JtAf8PdvR`T{i~b$BTjiUq7R= z@dM_{AKrt0vKM;$Ang(ush8&Ue>RvUSj9elEj?Zth76qgJ%WCWbNO>q#9fa})8V`3 zlYiQI|7VMJ+$&(m*ySSgh)u5m;2Q#=TF3q*KHS@@20cC73*txh=|8*v--njppL4{k z&eMeb+bNsV`=fdFs-%sIV@TwQ)O5Q81bjNI;R=F98)ujln6B25xwP%V{QNo0i1H4B zk16BI!gI&;RK&Z|)&rb^*wK$I&qyrN6pf|SKFoOt#cB)5I$lVv$8m~q9Gb4Y@o`RP zM@P>)Ta52w#ua#)w$KEOHw)v<%=P9ZXtJjh>Gn1BY+a?eBgzZ6SKLdvxTsRIZi303 zUd%>)P^nqM(Gk<%Qc^@HJ%el1QUC1%IK_=4$c%3By+8UvMBJf`7jO;y>{fSrmg_^$ z%r(qa`J`JdD0U~b#LLYs?Pa^;{@mPL7h1z6P2{3;mn+7{VqLv6S<@oD{dfhj3Yk+` zUw`6?Lu;LxUP6y1Vc=3x1rBF~Gm`b49+0$K{bIz3&|osOb6&oDckE6NB^6f#Ul zr)hvjqxA{Ogf0(`RJh3Co>%VO7{P}HKsBU|Q51D@(B|?)QxdVt)8@QW$8XpQ7rfU= z1%*ZIVg4Kz&!&C5wz`@;-I0Wkr!5^#9mQ@oRH4y*26p4WOpa8Lf2j1b+sp`|VO&@r z8^ExKl@YWtV=#AP7`U!_z|zZ#0P1L|d}4dbTf_8#RqKHRyE;?0ANZX~7A97srmo2) ze(ug-8_lfF;NBq_-HRT6} z>3uq-vvd5jb%V$1>V+~494aXuD>80?*I;y|B_}6yPMN&j3RswPS|GuX$+hc(@vNc@hc1?K0i%+^;yV=9;;PVxRP`ufDx*jT(= zl^1fWxjiv4L|f=Si?%sxUdof1x*o5A*6?==g(A5n`;?XAPN)ZHgig-&Ua`}WVtjsn z%;c?6CWlCb;kZ8w$(mYN;J8yV+Z#|Xu%4?6gGDSmx_j+y{_i4@E%Q}X-(zLNe>Gsv zI$c;9s}!Q}Jvb72->*Auedu$bhScKHy9F5!T%4pTX=rRrW2K~y7VN-YorzN|Y`|9p zF=h*P?by?u7Q1dHF;8&m4WEe}S9*XHSF zhYqea*VT2J)CG)_Rm3hSoK8!ua49V+%y&K2HS++&WY#uhs>^s#7mlQoYJ8?V>A-O` zS!C39k+6iK#*16Ilc;SLYMVMaIoU%GW7R3y1@hwK7X}ncw}?6t4auD?Ja({2kZkxl zuQ6I5li7>4*xwTy(<67c!1Su8r|05FcQZ4y@ra0s@#5lQvlLoZ7CD`@UW!6ZetHs1 zba&f(NJ1j38~0*S>CDnod=@Ui)XB-@J}_)5)gK9r?aR=Jm4B}gx5g(#?~*EhZjWrZ zI&EWQuA{?2S2lckF2~qd%jdx@!fZDVG1;q^g&qSwom04QN+E)f%f|sCW-9JRq3jQLXVTvlX=O#Gx&u4eF#^JW-4?Z?4LLbiT zmh%PPk(nG5GqsCB!CBi`2dj+`EM92&xqiYNzDOz_{V-4LPDy`z`;Jfjy~gm&8JlV? z(glZB^`s|*%#}77 z+YsD)^rhOvzBhcYZ-ekwj)BQ@0Dqs)%i<;aOtmU&tq+*MG`MU1s0%@>(+TP6>1`+W zM-Q+XSXk&$jllPA3zi2Vz@%lai9;rGw{Qv)tU>)fKt%0RaKY5{C{2C_x)S zO;pmmiRDh6i5Dy^a+l!}?0t5B0BK-?vswiA_mK$*N_MLR1aX zF3p>QyhsUizjV^xa(G`tn@Q04_{_Y1wLik*O^Onf6o`3VbGw#k~3r ze*BoW(U*%W!7jf%al&Q4lT-1osJ&#1 zDD1B9(r`xnPC;|@;X-6-)hu`;X)_DNtLdBsnme&(g_Yhv|9Gj~33pT~A@9MH_4eEA ze0zWQPQ(0?cgEHhsXitqx$*7W)XXwV#WPl`p`IG(XiqY4xm`1arlssPhQ?FtcJQxR zz7H_f?;PkkApdp`=GAEGMVG=C?T++>u(cc*ZxG5I+~z(3u~S`A#p9*Qa7cqI5xYf& zMKKEtmy5G_m$pd8s3Wzv^qWd~UquzwW}^?t-vV}Z(QTyMNhV6H(ZpDDeW5G$BH}p6 zBv1?(uNxA!XhRMTxko;x7jJ-8^IvF-l~5ujgW!ROY;WlV1qF@BqXQ>f^rVBPlSm}e zWQu+8e2H)ZWLXM1_d06mkhr)%78n}B69n89Da6Olnvh|rmomlp4I#hCa8$&^F zj!ra1$&-U9nRG4Q#*`9&!EnIbM43#c_40mTp4j`_IRW>sT~0b(lTEa%hLIZ{Y%-|| z-|^5O>Zqc4TW1w4O12zHiYBSPW{zx`BdeIrWavLGP#~eS= zB&_z(wu$F0~Uej9(Ov%p2zQ?*hAkgM^}Ojni0-k387LuQ)VCiXhA_EV44SkQ|EQ z8J6y-Vz^k3sYO(8TYSWdplGC+cHkU_(%5hXuC1t~v?uhd*2dQ-sr(S<=H{lRBlPs_A*AQp>_=J9yVuXUo{l8~%%*j|s) zq9PkslMHWJ6pY*oRgnj3?T_9f>aRq@%N@&E{jUNknUKRa=0nzE+Cs(?pyCV9J?yii+$|uue5fWcxh>Fl1?*T{O=u>_tKX1<7wh!E9)04i(YR&VgBG|y z47=Xe_FWJ0%lmiSL}T*|@TC5{wn}p|DTR#7K6>-HJCN@&ZcH@Ee;2mr2LmV+s*%#!GTW8P znE1#M>3?ge&Ia-BURRV%*ky+A4sPrRI}p`To7DHTUcIIS9v3IN4$N%64iTjef_Aop zv{q=~>K2=U*kcA4l0BZ-Zk(qL@-z>yCu(jkRk-(nT{iPp?rt3+*?{(iKnaqaiKXSR zjJD9s%*^=CQt^Wa*C@UiOB4!~4Rjb&#gtB)YOnD$|HUCYKN^Tuhlr@AAp7J*%!@R^1eaaHTKPgXmckNV)bY5xcWe`#_GczM38c1X^ z^j8D9s0?>x?h(lpw7Xl{Ab+cQv9Qpj8+!ODuF_ZN0pF=&_y*GI%u|#+!fsi#mAF>sPOSOuTWNqr_ct zYJR$p5*!i|Te8utW4ZB3@ID8vIWZN)Xq-4@(#SB@6ql2}EY`5zp9R9mFeuAj0+-Hq z>P$vX)Y;V5*1k}xUv7K?qJyEa7JE9@q4n7n7Ly3Wld3$4F|Wy*HT;qJC`PRHB*)s?(5uAqNoOcC@yhpsugibvXBg zhD?*Ntc->=Oa8R#r|@cNSQhWqd?N)NwOsdnsHCKW*U^!qEBj7E{eAJ9X^-sUV%|kY zVn}dDKby_Q8X6k<%Z9gtc;@b|Fu%B{P*PBUk&AT@Kj_y2!lU6J@JtAB_$6DNrgC_q zoTpOeMaue}mKJI>jTYmF%FFu$r-N=py-;{aRw~3I{h*2x!ZS1SyREI?hK+g! z1*Hz>Lc(kfHQVC5X=%#tcI%*qH_z*D_kc6jXkyxPbNRPsD6E;Jq{`VWb=A}2+CnRV zoN_{W2rJEmzuUqm-1)CoRb73F+pEXQT$pDsaYUS)oLth<)Agdn0z~SEMnJ9#RAjQAXk9Q ziCzE#o|CuXH^7=$=)yuZ^T0MK{4=0mB)Ncv?rUjrIl!N@*tV(p#>Pg4Fe8M+)QbxJ zWDRI_C|k)*R0d7nr!54Q@|OOsQ3iq*`nCKRy=k!bPEyCrB)@Vf(C~BZ#jLI#Xup|Q zW#0S%bMD;T2JZ<-+yby1>Ij@Z&*M#Yt<6iGPLRnO37U_Y$TNzgQx_FF&R8xR=YR~T zr+?n>g1Y}gYmA@Tb9XQFkd(HN;NEwxP`>W~QQ^o)pRWxo{Co8dV50lu;$ra;VvQdj zN}jH*z5e#QSyAJc=Uz{I{q&??)noXJxt$#{03_A=2coZ$vvp&Eg%iO540Ltj6JSke zwwcw4Od}90($b7?_02aL7#jzvNgKCZk5ezWQ0V1zaOaZ?w7t15_()I|1;XKQE|*2#<_sRim6Vidf=J3wJaa)&JOdN6aTy08ya0S*b8%)7d}Ex#6-i@H8c z9l|60ogGGpKqk9b+nWQC z_L=_txI#7qb(G*)?Ri>B33h60E=So@Hr)H`$Bv53%ws0PhwAUb+dqu{3NlKNQxUu8 z{q8vTW=ptRyxU;UIhM;eICUO4yE7-ZySpE#5!gDrQtF`RnJksO<@pRUBNE+Nk>$3E&Z2WHIWJ_u@MYq3C9#g zuKzf-dYgWJ*Dkx2?sQzxPvDBrR6xQFU}Fkazicu1#$>~amlCa3Q8lL5Q>(hgi!2}) z0~*7noIvPML-Kk(k!7Q!kuMq>OYosRr615adKCYRZTeVjBYCJeA?vd^}64-5vh@0WEl2>)=zTiU}fw zee>iTRRquNm5JD9yPyu$>gzXEhjIGFI-Q%6&)Rr(;_>t>5N^!kh6w`$ekJx;4i%_u z7W|ZQQBYJm;{uYiHV!6yV|n7L%z4$9{3>W1h-}6IV24l!h*Gz= zvm0j)J0y4>=?MarJvqAN0ZMH+0Ka!O#$$Ndl<_4*SKn!-tT8e&mr78}0A(L2p;={T z&zjVMq<`G%uCE+w^uv0ROV2avVXw;xF%aGqBrIqS3vT9q^JhwOC7kkv;Mx5c6sMl) zDxS!hH#cRH!OpN$VB!#hpmI3=>N0#lcHqmz=?@7o~X29uxbo~5nOXR3*s=-^YYqB5X`U&K$A3Q>?UPx`YQ_1H{ zy1+bn_lSaAy=?=5iD}=+n_8L3aL8vtqX7f;E}giZF=hRs-`kEJ&~T^( zp>BUuau4fhst5iRnfuuBf@a1Pz<*hTWO5c{m?^9G#iC?M>vG07B`rc6Z@u}_mX7fz zY`mmT-qnMn4{N~hLxl--Ql)Q1B9WPr4-y55V*~;#@xBxYxEs$bJ(kb=!vDag8iG;{ zuTG9V1WPs+{7>U*>54=9bTn(-xt!(JxWZi^;!EQHJ}&?K?aL=&aeI`bj)J>?=kRB& z^g<5YP|H=xj)v(AL(^$P#N!sSnyh)7anVC)uF8`^@hB*sfspLh6xc|g0O=_L#dJY27u|hGi^chK zwxOhYLZM!~$7#+Yu4fA>E-qdS5|)wMP$MKNHs;AOs7tCQMcoZ5l>=6ueCkt zMFvQ?3qdaRd$m78hMvg~&93EmPsE7h~KtN9qjr|u+#1uo78$b(}_A?TeIQ8V^YbNS3Vz0sDIHp z^MGH1?@TqA?GFfX=6T7)w z-PBzDtWv|{9Irsg@%_l4!9l&4f2+F&2cu%OKX+0`*FP5UaUI5X>3Ygq?(>wC=~LH#`L1GhI3*g}5T)`sQ@K~cqp8XmyZpjy1Xwl1D0c%7cuPO1k*4N#sVjWP+n{7#Pa zAfCc`_aU2OQ!9S&Dk=(5@3{+$h`9WMhi690rX~)6`_I?jS+^@m7T&C@uDLv&cURW< z9LQikKlS*TzOZnMMt23mM8H&{Z4@a~7gU=M@29LVLc3AVagp-E}|DooUF1K1aPIqg>Ne!%Yj9gkTxga$bX z-T(5)-qcU!c@{Z^K+>v(eXZmdk}#Oh+YcX(;HFaX4O+E@F$ie_t<@w^f;QS|NS6Lz%5BtaOoB zx%f*}Pfy`MM4ODN7yaq%?BPt1rMF2zB<-w%-kl5=lSZ!1x62zD%#YTySwP4VwakyB%_U(>q&1!rL4WZTAv|`R6IEWj2SPT()`Qz=k7Q5 ztOA$@OFci8+40FuQq>FbFl2L%r0_jOqH3JX9p}lbtE+bkF4D!E!^KJ(a0CnVw$$`I zZmaVgHz%QS3bJVx$Whw9&_ymkaWq5a>@<%?M{kh;V2s6YYH6;0(V-6x4F%Y4NNENv zS#obHi-umW81Y!jO|iEo0j7Tzh_0QToyOxKbgZPDVVa*?e!w>Zk8G@_VLlMQxi8b) zO&A2=?*nI%$YXtsS*-a*|AozCpsw5;0OeZcv~St+Y&tCLY`7SJ$6IVl7V0DjUYpGa zjO#b}v7(Piw5j)==u`%4HT}81r>ZJ?yuul4mc)1K3^wz#W4S85NmRy5sy;>xNOm1i z6n9;{qoczJkJj7_6p{s||MqmM0h2eU2jR^A7dUW^u2k##`9h}RXSqad3N`9KU?3=B zKUC%C=Pv?ORp$;!RV=+`DxM`1`1KdUgEC96uN<(Hz1b;l!1zEL0@oQ>qw$A>pit*m zyVQubGpDX3o>Xu@d)d$Rs-BRn$LJb0nJ@SO8E12$2o>f^tZFmbyJTatd9%ID0j{?9 zsAZ5GKYEudxD+%@Q-kk+|!fzNxPu4T31%}Uk;#s>|zr}SL zT^xoL-yIy%w9S&)b_krNK(_tJ36!qy87i>7d>|Nrq4xv6>_>=b``sRt?lORnvMQUR zM2#C5J?dm6XbFM#fT9|;(jp~OtbKzGaJhkNR}p`L!j^E{(K)a zj#Ld>p9eI~4hXNN13pzB*5VcbU%VC;7RD@VleVz|sJN=8<}%P7_2Q3?kVy^h?2jv2 zTg$J9>S&ymjUdL5d4g0OtofIn3dRV3v$tB?OR~THJF^43(YY(nZ>*sh>EzI$dp2sB zimslXQgz-Fjc2!Ka0ZK6msgs~LH~=)AduJ9{0n*6m{y>PS_;XA^2TUOPETBQnqn^g zu8m%lzvKk)`e>!QjCHNwd~!>ygv@T)aG-vTgX-8CsWo;ma&nTn#cy}AwvlH>iZuf1 zqbmL|!{_FiycuhmhL#`lKZT0#aXY?A}Q4us3X9F5ulgjsWfyQR#uR zTSp%4!b+r+tx~I-RuQD{C*o9(Tue0}QIJ^yv@y(dBMb2DuT22h?X`l#V6bc)4&pQS z<#hpqjAW(nBrYy23~`sEAX`lU^9^k?#!mtzH|b#XBVG_Ap23&`dq}VlfO5GPb%dm3 zx4hOc)rjUig=c)xn$LH(yk|)b4UJu&pYPgr(M1btWlu^oI37GD8!i^hRRnrvvNMyq zIa+W9ZZ9(k7;80Xj&Ttz0&PfX12!-u%N2B|H4hX>D)1=Vq3p715@*VR@8Dl_FvBEl zr+OH7(F_Rl4gVKk^SI~5qMSXWA{GxZWo8-4%8<%QLa@ zCbnZ}h^42_AfQst##zz?6N_qC^F$LveQ{y$ZyzcR=XxAh;UF1m2RK_whyog++K(#| zMfvGbqCBj4W4S5Hc+b`k=UxJE4jNHFD~Z*neAYWYQhI{a3PTuL43VR+q3nSHV}`hy z^*_FE`>)7a)Me=e)Tp!$^17s?q~44#rXCQqVk>~sbSYjim{Q$~BRha*=5P2-@b7>u z760rT%1o{UXnT*;EK&F`37Kp3c-GbLU$w_V= zw!tL@56uanTj^F|k*LI3>os3b8)6SyRBIzZUr2Uf;L;%gM4CZQ-e!bMFfx}KqFm(> zl#QvVxPY8+*U^mP-dcF*mNs(5rAKnG-O-rz`MIGku^Qt0Q#gam(KRp3Qt{+|uCMp= zSecYqIHVLd(NE~?uonJ3VSzQU^Yp&Ep09Hma*M00C1bG|%tTmNSgb}!*ayA`IyT5@ zvO;~jUY@6r!ZxWRU=Oo7KUNC}*Cz{r(tAL9_VD4nq16QF^Uj`3>$*z+onoN^URRWt zS2V16orbmFMAeHI715v-axIq*n3-|jUES2@ z$I^ET+AEw^OTQskcfbWdv1i!>e(_Nj__ z4NNY9envCM_bx&%aXZ4o9ruFC%?3FZ3&`}h?ZdL~eU17RBz>kpEa&tQsikkxAd(@R zDuVdEM@9Yfj#ZeTV1$q?2%ovDjLp@K4r_>1ZbDms3O0GXj*t<&SWK73E*k?gLD>+S zX~4B$`nuobQZk6*LxZ8yz2CXsRfI z?|!*rH1k>~wkIhi>xNOR1d^JLN`=P0^v6UMh9b(L4Nx1NvHiEV`x_b02 z_CP@YBMb_?c~1j>F48|0uxp9H~rDJ0N4OtciH=%?j|3#bfdXLpOce`wekkT zcg$~pNpkhD4FwzTTSMaQ-a+LH%IB(UF+py>+c_RxJWmH~8H|eb*{}B8#rMb>&SvaI zaq;d5kmV(B6UiihHQ++-Zd#{q<_TZa)SQ_I(Ls{m0n9H10T+=0Em5;CvDnxsV#@b+2Vje=`Ks zuRwm`l@t}ZCxGTj1S!a6zA-|Wbrcd-9}MzG1{~ZVvE6nsAp~?eEggo0ZOPXGE&gRz zRu=k#Q^!hVWaN3!s%yD6U}DUzt*T;1YI%tPKblT^_Nw}W!6K=A#-@AMuFPn#N(F$j zq`32Wd%B9|hz6hpMY{zpa_Z`S!nzZ<#ZFH0!pVF>GS@4UaM{=K(}-9PB!01qr~*>m z>Vsixy=hgyFV@URPtWs$UBdPS7(cH-5K=e6{tpsA-Bd71@9fGZDLJ6WZG)eO?s=BX z=O`q=ajMQcFq2CxR~YB{D7VTpWUcvQ{cLXzD`{Xz<)G+M2IxqG_)j)_+JWrauF`h~ zuLoMKTC2T~OEI&TpwYI9hk6LMzk=>5f`2uchid~lZi^~t^X6Y^jU#IQ1Qxs0an~yT zqUol+=Q!w28XFzcz%sxKSiH06b`5`jAmBGDpm}R-z3TNAH+RWfyM^nt7*^Q6;CSgw zf)5V|SK5u@m)bw2cX#rGD0OKBW?o%6xDBrh?a|(0@wJa9y}&pRzBhn#2Ted0pnH=! z-=9}RD6!jIeT}DMOi9oNm6rw!5P_Qhv)BcbI z4;K~W4LR>Mz)~1z9wFAb69}b5HeZT6_KJ}-*mt2aFW1SfzqCS2V z@ztb{{GuUYuoolpGJH+7A!m1y64{C@z; C%{D;* literal 0 HcmV?d00001 -- GitLab