`O\]^!_#`&aL)b:c6MdReKgf"}ghijklbm;n{?oTpDiq_mrstuvwxr yzd2{8<|E}6e~~JϧǽO70AF<++%  F!w&)-E026:a=@AJB"C DD{EeF2G"HIPIQ^kMyPZ( R|B}~*7\`@CGt"~$qc  m5 Q\k[*.$ a R   i G f  P    { 7"#$x&(*-.0 236)778O9 9!W:"-;#<$H=%>&>'?(@)@*1A+A,A- B.C/C0E1;G2H3BJ4K5Q6`R7JV8V9gW: X;fX<Y=Y>Z?Y\@\A6]B]C]Dj^E^F2_G_H_IaJwbKbLxcMdNdO!ePJfQ'gRgS|hThUiVjWjXkYplZl[m\]m]n^o_q`saucZvdve@wfwgxhaxixyyOzzR{{,||}}}`~~@nځFkѓ3˔lɚ)pHĤ; OMaaaaRaaIaFaRaR[ahgaCa2dbd}lkmkfnk$ok$pkA$qkLH$rkS$$w S$L(

PNG  IHDR&N:IDAT(SI/CQ_ 6XX"$j5THMXAՐVU hQo,9ϳ9P DTF?e?1)CQW|~8NLI޼ːn!z89}3jؗAJvwG"lEpC.[fۊgk^K?!s K/O2OfqpEV74:D,GH}. vv!R!yDGDĊrC48H{Oh娛45b6$\n籰ےWȕȠ~  W6d;dZ FPT5ͅZXO8]gdlB4nߊ#w>Pgg;̠u[,*e;5 IENDB`PNG  IHDR11ٕIDATx^MHTQ3cMJ`nZ .Y J..Z*H&]EDa&f3 r%ЅЪo7hX3xޏ;0}/{/ gf<B9@wծr509֧9 hַ\C؀itC<6ȇO W"&bA گLѣ;MB J|9bF@`:l0M U| S&:$Xw)iPbJ'Bi|?NVFVmy6( )N V|eQ tOǼZbTD&>BzbI7z#kz+k2 DqWS6sCT0/l"➈DĦLj7yb"􇩝E+2;j4"b3+"\𫈀M-dD86,"+0Z5k V"B>^t>n䙻\7.D *.9ovD>҈ höU]`Jy"r9XC֠4._1(Rj 1{,^*@/qu _%@uu<  DLm#l6"-$Cw IENDB`PNG  IHDR11sAIDATx^=,{Q( lBA 04&FM"&BRSDu``2I :HL.:4m'紽Qun~EG:}?a/ "С x nF[^3<›[Z7PxSʈBpˈ/d-2BFEŘ b[ DDKʈ&F 1#L$$1Sg #dT9XoNHep&F-la3\z0zcC\a@L@s|CtI촉@F!#dDCIENDB`PNG  IHDR qIDATx^ATqyYeY"bY":EZ"JSt:DZQ5EtS=-tZ c^a;L3}~|N_;_۪FYMWT?xfF6lFs?T‡$~\A;BnUF@A@A@A@A@A@[c1P[q ŷ1x7c%9;)^^46?oz++ƙ 9c3;¹BP~c9jA.^15ߍֻKQkMLQu?] B?ꞥ(K^( {H;5L-gQO# A0Yvg?FF@#O# 8A#qw:O#&oA hA#7 FpGAp44F@#hFF@## 8A#gF%4~h(9 A#ho-oF#Zmone+-..Blv2P~r& ^_ݨsw'6v4l5pm~;—xuͳk~]}p;^0f5G8tA`Q"y! &<ɓ&`Ę A "(iKV\Xg=I~sg[kf;]Y Kr5BW~3TA4. ?lPD!^_0dx̅ 0p mv fR vNzm-q3^R]ZGBFEu0뉧 8跚qZs6 hkTCk!E trd:HvIAК .aH8q5a(ʝ!KYIENz30ضl~|5ܶ@} 8ln;`m!&2G< vrL; :Cω|].3 q'PzulzT ^hH@eC24ujpʡ킡F uV$O (B< UAOɨ! yG4TKh`wܠBJr"j@u?1΄6hI)17jlD ]BoNkiVktZFaDsEdPck Zd!n7o\C`d!0 X'$d2,aڶ}qg`): A :  @G9uj#K! gIs|U4& =mPF6Àa2aCaЈ0mq}`G<@!@q+7O=1aB8?zxݸx9B|臄%nKsK70 @PXFpgSc vVK  (G!R\} 3uqx{*;VB& <ƌ?9YА|#ƒu &rp S\5[vdlbo!^RΩ XR7PljgrҘKA GՁA!σYBϟ_VCKXY."[nZv,, uZey 9K/chZ.(IjV<'GƝL4Q1!WP5N (ڐ` ܸW.ò++vNQ9=ڃ_}D9YL9m[dmd;wa9tB^y%y?{X^ {A}`yܕnܺϿm}}Gw,kҿ19oC'd `hHg!o粴륻!ykʳ"rfcZT&@| $C`fzgd]mne?-252}%: xoK`+|r@0SRV>>%"eSݱ_v_S6ɜH$שs ܪnCphM$ vs=C'_>}vsnsXb}6Zm2r%k.KH2rYΥxaU$Hs%զVZsxYFP_"̹l߰Yh(ੵyߞҞ %ip0jdeYO]'E@9(z v!WR@jXCjh($Q@@QBj F笞@u P}XRAPah%Ovm!(H8!5Op(N*vnn=gCG`ni/#BU1.ʠ|bx!#?/@ .u4+*VU, BUGG; 5x1!:J]u1}H%6#r8_?8@b;.30t"B.;'tT+7+u9_ȭb\ jR0AnB87/QA,#T)Ah yd!$#abq~Vޘ2 `H-^hkx0!bzg=@P8ʛ|XjAe[k;W ÂBTq&V> x4pXN'JANk`14w AXF6[̸5F`M^9X!"U rUJ54#h csnJ8IUB;ԣPOe!wA0kzƃByBd [̥aa`Ф~N 8僯 ;:5M#~ZT <YK00>ՓƎͲYɓ2"y]&oï˖][u_1b+oȿG8c >> hN02rrh;vioAF/u{d<߯*Shn.i9(s,Nwc" Sڧ)9.s,'2qWwC JI/|+_]6'm~^hjqLJN7=}DJA20$x\[ 5gg~FZ1x,cgQ91L{qM'ths[5;NNB0&&y[id)bk?.24&f 3NrmCK %]F*qgp$p^9nаz lYTР 880``cUCM4a]TQ3.2#Xk ur C3.BO'&>Fp 3A5 q$:VP pZ @fVtnN nN l3u#8}Θzro%g ޷Ȏ X,wX G(wm۾&^j <], Bn~N& DZ&ȱK`!`'` xLp^Ma!ӉW6Gj0#vǝ- zY p'cy YG1@ L<9 {ʁL`_H=vw HqGИ B׾u/Oh=`7Q0rvv8L0/0|Kpюܐb7(ppWP5@'8tU L|Y, 9rDz҅  sr  WzmiҎN`'$ɁNVi Gpq^S,`kGBn(õsj BA8h` 8)Jp\ +/H/P?00}~[šs &'Xp@צaT%+ &+  P0A3'rx!4e;E8nPDt3@~!Yr9^Bv "4 };^'m-I][GB HaO}nSh;ѷq[mZ@k!E t TZ=. b8i* I! A\MzgVWR~0YmkrP[?\5+O\`9smvx[p.ag@疭Oc5.ÅYy!! V$4h]Hr 5A/+3X*ž[@qAxWB< UA TꌽG0饵k|  !{Nj-"bAgJa1` A7])Dk(S7m+ !L4#z[ C5:9y]\4C&# @$-SW!0S@hY^&"AHHAm ; Ti @U/NTV9P8LXkP p,u5xP℩&^ zG  0 $vvn :m1 =uTGz!aϢ!` eG;A ٲٴC"`P7Pm8^+ hA SLۅ}Z-K&\$0V1 #׌ BC!E7\7P gW@<<\$^'s:KCX.@jAA]!1ׂ@ Ձ߃Y9  CK =`N%9,o uvxchZ<>3 `HiY;²Kf}J;;_c.z[xhs #3oCjJ"3pypAx䔝G>i:bwيʼy޲?Y'H08d}OM[a9{9{;iϟ` /%4KuJd$%EoAw6jӿv^9{eGP܆7?m;hbu=fPϽE u.whh'6g+w66fvkz5f}vB$Ը_Zv7|+GJzv 8}?hw{ؚJXG1wN\][Waۊ=7<\lo!XY;;i'}Bhwg|:a-!SJe,>B' pA.Yc+)׷.}?!]Bjhvi@[sso;zlSÂ` C@6֪0%?ܴY፛a:lOL8 |{Z{ {֚%jh,D6tU]A QtH3젍 ~/)?(Hrg}Tui|C-\pÎ` ퟳlԾrtGZǧa>s񩬂` ެo{J!4}ޮ Nj\!+p9W8^0>rvp)D9߻tC=m|ZtzLydy3dtGa| n9H\CHXZ>N_Fv#@bg+,$ '̧ᄱ|T=m#s><Fs^=&G0Ua|oU#qA:Brhܠ ; 7;e| |Hpy@qla Poy 9B1Ɨz8`fܡ>lL#(N%*B5ȹuV~ r!rz)¸!%(#/B5Np#zL˧N/0&A`9B?S`|a Sh+t;oaKI5ZGc24ykr|{/K{$rSbp/W2keѝwgU!48.v)40j+ܑ BͲ ΐ < @94X^]O5CT0"߳XT"4 94\i#^TV@j%}K\;' €hV4{h >XNPbP*?/pVNХ,@`QƗVt)R\].Lhq| aa#>}B]NISbӪT+D2b`{+.;P:B !TP8'JJKEQ bi9'(A1pQ!? WӪAR\|y hr5tӪP$[04ЅJ eK6!8o?*oLZ!aXc#̗ Hp+5-JOs4\5<˛[W :ür/C#o!*B`Ý D &i>x R ڥ 1x O 0Dxl1d,ITG{K9!"U r[LAa2%W ry7B^xlݎ睱m`( O,!54T0 r}2s¢#/%:PO14\‚u{Ȇ4/vpK7VCI_ş y|_8ە#l !BKe4' 4. bVyWt ۡ߯o8U4ƒEDjLKF)}1Atmym[?|^5EoW5svX崽XArn{^xNۚ_^KPx{vn8 "GP @hNF Bpg響m/.~^K{]g/C]]  n+-=\ݵ|Ξ} {v4gKj;2;Rw lu#{/v!;R;f?%7:bu/K]Ճ#JNC0$x]MB$.,3/.Aw ߸hW/_.Ni m.vs+YRmƉIid-&5ݸ 5U}Br-@$+& ^[]Dij N'JY_3 & 04"`Dg,vYMİtQ*v@aPe<,, (  :ɬ BO'Rp#9BRapk()գ Ը.[~)U`ͩN 5r[@YVP^It%8,[dG0,nn;q,DgVPZ v| !|%HGa+]JHebI ;p+j Gފ5Dm"9'`8G ͹G= Y$A 03u`, A]" 'N;;D. 0 /lX!`Pqx,^!Ya#h>GvE`0 h *\B+>zC''P'= GpRT8 @VBA=VXpKbjiGQrta}l8BXqaw`/ pA)nZ{*E~:A0PυSKG D0TL2'^Psr2gT0f8 G{N:+/ J f#cS>ؘu^Cc ك+m: 9i'nJp f#1UScI$3% ]$ҿt'yҴ8Ns `IȊ& ;;; 7=`I(n2p$  7A.ph%N} &#T?]}sw5{REQEQEQT(ӑvQIENDB`PNG  IHDR qIDATx^n1D׻|"OhzRW19+&:~ozwA >U€e aa; HrYa6Բ 3 a2 0r7@0r;=HkNԻFUcם|A`\ʩVl6!Cyә! ڂ"5La~zR l+x6 5A LPO&~O\('< 5C!hP 2 !!p<$W  0jlvrN{ToIY?A05SAs1P sԐnCB \ycTNA2@MTx?7t_(2&L$Au(%PHJ 4Y <. JJ`@@<BlBASP QB=>2;1?Y3WʦBy14@1' xBy O0ܝyf_Xc&.\q]E2%ZĨEAZ(IL*FQ0IQĢA$ʢ@\pqFaݗ]ŭS]>iۛޮ:=r<9fTZ<cE["~ p䇗Q3B rue>~z65V:E&u [!AR`Ujp0-COd݃#!,2 ۱ԉE &I| GZ[{@W`RHلw/rDmEJ@`ΓGDރkh ҂5Eޡ Ӏ =ç8ct|9[) l%)Ѣ 29xJ@:PsU>Ǫ}Kh|Q[ ɀp1{R\ B ^מp搛mS^B]@p yC] (yW+OY=Id:ZX`I\ SW"Z5J;&3?ڍo^A~w>£su#M@MHl@f!Ay8Pp 2ڍ` Ys ?E7 ưNSzq|p0!M_x@`A҇S B URRt"c؟C lQ ں: f2Y@_Lx{|)e$& B(?4lG{s%ai!^nB)}eKBwbO`c!Hb쏡ę֍V9Yt,cj(}ITFM":9<ͬ\U( ()sX[[@6~<soNH#|%R갭$΢YTsYT2 P*QۋޱbцfٷƓ|(`+T(lLZutw3coJ RhqH +^7KKsT5 ۑ`[ (\FlB? Mdb7t|dC 5lëؓΊ DAwx*`5\L8_ : , ( L;'-Dӎkc,kI{W 86wP̿߻T|G!L*6qeh|v3| h Z"  Mr H2µSpvf1$ Mf]ע §`x@| `(1cؐH:'-W?FPI^qUH iAPAFʰz\2DY<{=rlKe C fe.ހue&{fe@Xpƌ;"doAI=Q4AT ChgT@T0jL 3b@BDH{ A"pGW d-`0_@2Yd7bM5{0"05A*8G>=%xa; A0”4I%iyG?zWbڎF!G PQdC!p!kKv ȧ ap@ cw+ 1gN>?xJ:LƯp@A8* ]@]P7f|<4X`3A! CPCkEe a~q"V@1tDA ?A0{ !4& z;z'|2!'EC2 z F` K!HkEq50 ˠ_/C@"1tm]BeCp@B28 T| @@/ǥC)R}8`XgBB-[{wS!@*`ȳ!^ B*~9~C}!3{&~s @|L݁1.p27Y@.h ΁w|ue2vIENDB`PNG  IHDR qIDATx^oWƿ\/u$6LBPQB(A TUE<@BHihe}؞LG~Z[k9y9ʀFȀ zbHaٽ^@E3U,,E3g G HYF!P %2+Q@0G@XAE&ad}6 Cʂ sB9^|e@Bp!R#(TT!0F ,ӵw (8*3 8? ==H4֎xVA }AA !0xq a0c2R2B$8 UvDxS90zPxxWw \76LiIkH/#IT,䥁r' +I<3%dCa(-}H2@&10,72xV^Jc(!G;(}UJpS.FE$ ?KشhXvȒ >-C`(a4b(˿cقCAʅP0}8tg"x. 1^\)YCi#  \<EBJn eԈr A({!4 .4Vy ɪZFB>.мrR^!-8c@B𵧱o_{v"W%]HA%QCiGphO vfp$98Sn'~ 0$(L!("ZNGDP]/ղaZa\HQil9\.ԡ, xJ %5@l"@ =u3dʿ^Ƿ}25:P}i(R /LJ4??nFϚ@<{0Hvwo9ra5mDRa _GXu+gv%K!7|!0B9b 1PWg(W 8TbVKQg#r鳃 V"`TOL2\8V?qL( Px dMyIpkh>/.H&S2{Ns‰h04g|/ ^`Sҷ8yh8 ="{ ) o-%zY4!m!AhShLO;3AᅩK?(FRٔ0JŔc0WѲ4X(a*Guy yg(W,Tb聐P83̠BF so![Yh{8>~C7䝡~tyW@Pхܨf0_KT2XRC_DQƈ g@O+`wEL{.qL~C\t"kvrB1": t!~ ckAJRi2.PL{͙({5EhW63%@pC`4>dh%0%1N]b ʹ#F˄~IeH>g s!; @S̀(R^77{"e`R 10f,q-DFe T)e^ eykCkT󆒜o9e@]/ 2#!kA +2,` l}I.#0q"2B@] 覓il,!`U7~YMf}ʒY˙gtm*_^huf7Ƹ2#DEro(ݏU2b(o|6? UKAPl1/`  , %<K0I q?aA/C kIix2fD]`r^F}Xyc@rA0ǘG ׶0,>~YT%P oɟo'cXwOE ',M#01q9.Eznp/aQ c=4w',[YwdYl >m}*G9b@I .;f<7Yڢ,Z89Wq95s{NmK} D=4͛80[H q1#6Kp Y4m{ Jn:Kp\c`|=AvؒҰ23 |h}{/էaų#50 HiXY" 6^{&ul4h'Px' 8 I\6imYح'̽۸0.VM71~ (k(k9v6%ME<T<"@piC $mJRi'N 1urn2)쥯c'[Ҟc9VY{}l[;*zy~諡0>k;>oF[C]Cf 9A$AQhi(-2O|;?^lG8L,+h\0>;ԇz̛KkY'g2 ]}d6 ȧj ;/ 芣P:R5x PI¿o\ zڠ(AW:Aw7Mm%EТWU j434 ~?D!:$=g 2V0CIH @ְH"{PhTPBRVr}vA"F-$:V;` QdgHᐃ3%GZOr &Tf_ť/<=gp@nP;#,g5DWPV7hBǑkXH@aQ{Y*`M!ā}¡q @NHڄOcwr>)lSh{qq ]F47Jv-$S>]ܡ֐+ $A0aHp71:5,#hk ج<0aY-xrQ=۵AACBkAFS"G=zlW!3%wbb2,R*",c})ׅR6wUI@:9oNa1qJږ>28\U D,X#{y0\W%lOۓj}#aBXU1% AcK9,25eo(A_>s0au`v؂k -,$$e yFD8ȔBV0TW6`/7/=/`~UExF0}N@6 Wd1JN@ngn0{ H* D>Q*N6%IߝrtBnX *BćO|14'd.B$ 6 0D7 ~/X6e5 u)wք"_͕@?# +ӐdJ(EӆA a+ d @4]PF術g! #} ܕ`cIIxqz^4ts \ d!'dcЃx ٮ3 P(֑pg??#1tt|FEBYHt ;|$M@TA 9aːƒFJJcW~z7{3lz8 6|`P>hM"k͉xKPxkaq@ $V--:[( ﹫< } 㘦6+9$j $"05 sJJKh'`r1W? {VR*rpe-4!:_06poIg˸fy WN~b;'>: -de_X# ۼ^c8mp9/z> *`IU $'Ph5ȎH#U~c&ؼtL~8}85y 3|-U*`N݊@`M}x'`|l?VU 5eX,dnuҒfA,<9Ɯen-\q%!|h_HsOO³'ẻXĨǨq3Z +S'p2+`M6:H>7W*Ydp+el/إJxh!y<_g^6!d g0 I.+ $f@gA0 DA +xo,K lV-) T4 r$$$'XYxtK.A5*90# <ڻ? hk.ͯ@5h!0E8O]`3@uz %C!5 @@pQU `Wl*f>rص`V0(W/2{4 yE`"F2NE a0~ 7_ma?sȟAyxIA#-/9,Ce`B\ :TҍrP3\@JJrԝ<4V; r|)U<;p!a0z l|UàT!8 b4!PJZ |Ir?*$CA£PNIIJq9 y "1a@҃+D!zH6[F`â\y<&_DȢ(82zS>8AU1B.!} 'O:xRf,o;{T. X'NxHJ`O>kR?aup")K7#IENDB`PNG  IHDRIDATx^ Ka(BA&QFb(F(EQEQaLY+eͮ0slmٹoCg~v`<``````````````ADwQ1"LZ(." #B'n!&q>_{Kn NiG*t7nC8h p TSSNbA!fѫ'OUue!^UBeq^?By(FROų'A̡~gd#x!M;5)FyzAAJg-=wHvۣ?"[U+. a'ЋF!ᒴYDvΣxa?ڤJS!fҌ% FAXl5#lKCxc?&= !ԌgHǙZ,A6|_+4#l!;B5{rJ!Ԍ# !;-Ё]la wG_Zܾag`````````````/ IENDB`PNG  IHDR4ELIDATx^ad\Y2  !B(%P2RJ)!,PPJ J RJ%PB(%,%nwF&&mFt^o$gvܻ|L̹sֳհ ˰ ˰ jXeXeXe5,2,2,òaaaY ˰ k̒7,+%rwyfX. 3pb=B X"> </( +St`X|ORzr@WyM1X9֠X+g lu $cل,W'йT-_"b8XTU8u=󆾤a B)+da]dGQb )j1av%UKXE IR.QV8VVLk$(I±!JV*u:QO5A\hX?S}d.S {Yk0tc iA{ K"+V]':2iR =Y-+-d-{7\9!Yuluh,ݑ%52`Óuޝ+hǎjx5>%7m(oeXy'^FĹi`~okźM(Vx&c5N)5;w:"۞I ԑi? #H^ wNXn=/ARocip,wj撰w''= V-(rλoJ.+KD2XrfY\s`0mY+|_ op T١b9B'b[?Suhq=ǫdX\k5{$W[X^H*nHGRe'|/M"&sآDGiX[\X3&: Q?r&$ ƲsJܰNεhX[Afİ\vYv[W)Ӽbr ˰ ˰ jeXeXe52,2,òaaaY ˰ ˰<IENDB`PNG  IHDRLP/!IDATx^N0DrQ+^%yĎEZ(tz[j3)﻾eԥ񛩀up\NT*l L(ƖMB*Phic` L1se@R|; QJ1)$Ehg؋.rt!N],E!r񬘋j٥:@w Ȁ>*@)"; l7E^~-3"P`RH R@k/3`@2] !bl@ XMrAI H 6_8@ ZBSd \eG"!zkp~ttI8Ŧb\1Q4i$ntIp#t18x h2jM(Ic4r~3+O9G\MEqYTR#F1DAh?";oΞ+DB&sqnȬz5k֚*R6O볳>,J٨T2]pܰ&c%j*&3eT4 ]E# uKL@U'KYb 5TZg|lsx,XG@@e%@2 ̽E,2@T 9gVnXV8PTNo}H5dZ.7 no*V`>$[,SOmj! v U;[t᪓%TKB*_Yd}J`R, X2_>*T-JQ"Yhc bV8dii!kc d.;y+Z *%@U*,xƬϪKT*UBl_.,XDA>Gc#,aUQ`l P`9 %P'%%W:Y k T*i}Zsx4\^+l~}~_9+ ]*TjɽS`BZ_jᲗzRX} Z7g@@9ev@W$o9j:e)۳^{r.v ` dPP&r{] huyo K{ -H(TiJqc}X(M_f2Rއp)Q/$>Lg&"{%U wXloֻn"3<`WҽB%%pQ,<Tse6d- @K]7n|ywr2T)>c(Sn?tQWQ)EC%0̱Ĩj3D+ Ug}6yV݂0:j l&*.TbjOaS$;O@E@;[Q/O #.q39pP|%ꠑ筲j`5Fhg"OEIR(,HP̓<+{BS7gҍg1gש E ^ Дp=9dSk V1oO1eQ,{5 8\\-kQX ArVjit0e'sp vl7>j;+{@=I*ÄU,  X -dKBXB8дz :Q9`m: *2R\vVr~ͷpj11~;wYE *AưƋӏB$jXA=WyC4]Un^X-0^ 0dޅMDg90ttdOFt6]؉Qd5Cg>cc 6ڻSҘ $8ybnP U,pEC2;$+VEr 5ZuY{?, Ps+VUV;B|j}V諾Ɵmʵ]c7TG0g{3[C+gs%X /ء^b`bx+S6=|*Ãϔ=#v8l7UY Tk,WO`٢ tQOLkJ=z>:>'XNWPBuVL}!<VfR\Yu9V*VjeYʪV(sژuZeʤne ?mEÒPi0qjп=~ǂ=*2cΟv#Zin\vN۞Ҫmg0 +YV%> lPj}7@K2U$*ontZH8TZTVo3hU!Z*(^ۿ`tw4R:"^>XI}Y~HsPf9u[T>_)}/p EoԝCL?8ve@WUX|%Je/mkVejXd08sGѻfl?[hMVq~,>w?Pcqx*!@lL1gM( PEšV.,*'K WVl\L?yʄ4P}K)}T Q"v5=^[M:Q Zu$ƫzWk0\UN-ce0 @)`Iɡ,UܫT{-)G YdժUMTZiLb枔F&WyfHG;"Ţj\W-?_ 3cJs9|L85-eK &/Zm/J .TR*Z`e+W5`6X:5~SUO߁ b#sNQ~k26Ycn9kX/}lftCVጰ3W,%_Pa..u9Զ5x؝*;Nֿ:I aj>/Lwӟ-5]_w}7>z//xt^TWڌ;ޥe*5dHpGd_gp n2$ׯ9 C_gZC9 9CY8զO,ur 5R%Ҏy~ O,XRzJFKW`kWeMDb`I˂ѰpelnM,jR/ X /UFxF/?lO5#O<ϟs=PU䎓fUwnYgE  jZ|ZaJ7M:kW4o]/}r1Sp!=X4Y a"my{?,ִj⨀Tde >0`e˖5`߿i/5كST۾c(E +ݠzµptX+7sGUL/"E*,g:F9XAoF(Be tʊnՋ}fuL"ne[1+L*#/5`k,W$jxD5 /T,>r^ZZQz,^Ջr![l@μ=`b5֒%KdV1[_j_Q8`o蕅ljGUjo^5qag>TsA5daƊX',kl3i^эp~Ϙ[j0_gT3 uGn?ѵ{1vȽwO9"ZMg.oU\6mt,tFajv?hSse~d21,pf@^.c*zO|>裵A5zQ5Y-,XcTB4>6niY Q`͈ZŲc*?_5l&V/#+V$g+<k…k XN_Uc)ǿ!cg,Q.Q3`f:އ &0H?`Tn' KtF4,B8X)Q,IvreZE~k38VAFZ'ϙ zYuTiEl5F)N'ZPd_9n_;^>y&/# Vlir+l!B2oCZr5e3 , VСCS`Z!TL>T޾yXd VVݓSn2>h<=}f)|FkXVzir&˓"iBkC[ܙϼaÆ KuHPU}*#*'<ȢWո~D/BVFw3eOefb)f[sS2˓񚶗.Kۏ`H/?N5_\Ҁ%n6VxK;!T +<,' {azO1B ZB{<2U$ X~֊١|N:,Ck4t[VK%E VEw.Xyտ=_΋#Nd]}{:~ɋ 7Pަ VK*/uZ5aKjRX8XbE9v.P,sW϶2;.Q\`Ӹ:.Wb/cҕػ!}kճ։l]+QMW>|x Lij ݦT}lX_]xS؏x'xt<.S|އMAl-}EXֻv #{:::!`1"Lב݈=_Pw JB5yN)*cwь~WvGT8KYWjbT4g(Y;]g r'3}Lm=C6ʷ<6ךR؎32e[O%wN4Ӿldv'޸%/j_1Lm6B-eS[QѲGV3=[E֨牊#TWսƺ#]G\WS&!X :egޣWoPATɃ[hq' ,}mBA?_˭"=JolJiYpJET%^3B0c1'gved-70/07[Zs͉~lAqۼpQ&Tʂf`p%.һ&^r[p;%*nxU֖QͶ֖;ޢ*{')/_уaNRgb{<څ\Ql;ICtV`vvڇ._.D^;;ZLQTQQ z ?UwJN(Y~DZ +X<ī̋7%;N-*'N'N I<;LwRNjo!yb=#OT9N?nk{ QF3<'=Tl[jr \Es8n&ÙJ)|}u?ݞ6•Cvxz:^.P:9: 2Q,'g(7qs\8[@Ҥ 4Ux5ح.+z&ZU *^5e/OyN(\f7ѣG'B.}|_ }8\hutE,A.iP%!71}O6耕T-;CccA[BgBAUi30W ,9l J !_AVTy[<~0_k+SH^?Vz1tfla“"j= 5qx%o'}+T8T}&`QKQ=Cp%zh6; -z"_mp WîE8P,tbسvbx W Mu>zgv{%z *oמAl~)zt'ϢZ,{KtayU,;`iQV-x+9Ѿ7pe/" 0_ZYf.X5Y7J=uљ}+b6W??洭T'7ORB`  RLW6t -Nd NwYrT % g1Zly믏4xy.J%$V_n,6 \µ!,9 \4O'Pt g$@%&ԊiVҔ2.TU,.(cM_{ \ޙ#e:\>9Utd&C{`~I|Ù%e],9^|^s@ d"JFo,XfP(X@ZfP"ʵXr,PbYGwal 2ws=RjG`H~z"_qt M2:Vb`h8} *T-#j+\vQ>\O/J+A( ~ ҕmG(&~UhQ{ !lXE9v@%WWG&VpI)"K s+EP̳̂ :- PSswe鹼i{!PN|pU,TbjevTy`_N.SD:L+I){,J |EAvh f);3x7x+$pE>J$cQP}Xi5]Zd>L$.sg˃*Cř!IX`ڟ2 0@j覠Rj僵 BJz%}BlW+VZk~-jEd'ۃ>>ʪT`}&s(T@O1^`e@Pm/.âW&V8*B a* ĀT* Уj q{w XE4LZi,H4)y8*DFj*gOU{0 VJL(O!LPPeyVLּQ9TuXbalB `",*@\>BwBZb"Z`Aǖ\L$|źq0"X/` E N@D #^0b\Sd-Kp&DtW3<["[ Mq nDV}֏89tn hjߍ"lc@4`125Eib[5FD-Sxp X)k4L{ kIENDB`PNG  IHDRLP/IDATx^ \U7ޔq 2LI""9H a!ړ?%gӢiA@ldRY@DHBL/o{>8uީ}TԷlkιO{V=w8Jk?POkh=N8VZ!`j(L0 d,ac*4J`~/O7X2(I$(Be@]=>^O@ף"P2&$#"4VFm`e|2!rt~XE$D"=Ye.A`qT$Cj/X@O'>dl24ÒQ 0 e \x2:PFDZIЪajT2( "ʂaJ|xգʮc82L%dP1@]2)BPqxt2<86q`2KFe1da"H QUꀮ UqlMB&z5,ޤ˨*JDL-3W4R*M9 rG&qɰz-*RP>LN"B9O>3s,tChU㇜Bd\2^Rbd1$QB, NDGğ\r440 W9z-r@IR1"XBΑZ<RtH,4:\VGB*DbrG(m!qyQh);G UG>h2ɸHz3*ɦT@CS')@@ BBDs ce!s82bGVGJР2(c,@ÔC@h90\3't^l+Bư8,@fRɀ⸤bz#*"gP*>/3¤Aa#&s.`sFp,¤#X2*% -| Ӥ@3IE(hnARyޠ2,V BqT?x!őŰtIaxPr?ErPE%PNt0Xy!@{KYX246^OM{4B!"`t\BjI%ܤaB(mAx`E G#rF?,sdDd.FBiD ͩD#c!$2XՃ<2 B-BR(my!5;M+X00N* IX-%EGJ("\#*YeXT)/}4Ҝ]$<ˌ4?*3SJ"O(R|NsJ.M Aii!cH.0R}r$z`ɘ$PI)ů ,Rp̹F)R åc6Ƞ68(98T7=u%vPyfNe#Sª:Jl飤h. hȋJŰ,0Asҍ96 &_T^`F'h"\fdZKUU#PO飴yTJ)*C?Ģfzq<`Lzx \/l ~lG%C%\`AeXՎ5 ֟/AKvA`0-ҋ[ -(1w:μtflOp#QS=/ GK2Б6)U(9XՍHJ-u`sLϤrE\ٿ;,rJX f Ft~kXa2RRX =w;C2SJqUS3+]|EDTSJnХGs)HXJ9s3"l47 F.e dH5?Ơ̭lB-D9Pw9s5]2Ƿ܏XE`/,%"`.*Bj ǯ8,I H$3W~`Qt!z-BeJ`O\YFhNj߇T\ LL#O- O}_ 9v% Qb nզB ylIR `~!>zn!<s!ifFdF0**nj)…y8C0گߍ t@;$}1utEa/|'v`/KBeQ9x d"NBHF^H-A{SADhN@2ب ]`>sqa(㷦 ڂ{mEf!1hqa8nl|ZV| QEWVUɨ=a*$#X UPj!?i !>DwVb5-X@@y-[&5fpYdЄ-gOEIзwbW߰ ʌqo'lsw*Z.jQM:Ԥ&RQZe,&)2![yO!qC1$ U6+ލHD ̎0a\~ŷaEȳ9C#X$vPnj,1beHw3OIK%B sX..MhM>TCZ#a֊hCPT ide1r`BIp}i!.:Ǣߺxts *O/Z  p &֤=*$*sPy8v`4Ph1Stw5۱1K@9.J'$Y#qj9^ގW[ -.&vXXXfK-CdsZT1(9]tUsqԤhH*֫[exRsK(4O,zǝcy RnZ{zO;NrFD.,V*S{S+DYr!Ҫ]dT Pr@$-9hZfL߄: myR;+PLxyS+$\BW~X5XZyJ kCQ}M+ AE&STз_&UA%'ֶbX}zcѿjePIM|Lh F'xn36**g4b `.eHHr XJO< @гbeVt~#N_E8\4@= J#[|昤RُT UG!b"@ i:G3I8vu#w]xfKH/Ԅ&QV`EW1gd u_ RoK+EW*NZLf^m2rGA*ȞP/t$‚\ OTM_?M{n&pL D}i{,Y8zE7Zkw%Nۮ$`N)dgQr9}VbePѺU)VTͺBI?c֢| ^ 9,*ga e հtOסG#tS̏VӒ䐿 Ώ.DKG'Bj'ՄDkOc ߿J2(-؎ ǒhzi%-5Lnn@2򂧷2 */Ű25r,j,^4)y;na ^ϿOV`h"?~q!lx͹QK!X 9oe[g*j(5Nb).vƓVPhbۘvU/4U }BU;u0DwMhEU=OVwX8P0Du'`l"xէT .=X`P ْvc*bXHX' 1Jeg J CQVJjptLLuO *)w^r-+|h*G) =gy f?Y.^K*8|&B'Ut-=>ے^_j,{ZWVBT9H⍻C ]Ӳxjѕ²Ԡ U˗V1Ű Foy3Z#"U7e}==gߋ ^i^ԛF,-,D M+O9 o`Ͽ˰:5, B*m U\ gˁUZ~J6a"E>98& CUx7W|Ό&ljIT+Tݡ齯>[wZy56 zP R褥^ +eo8 a oԸ;CŰ:;`)U L 2Pjмś}?q)м˰*oޫrXw Ԫ7C:}؆uz3U&ao=}k=%&oWO<]XfMrÁ/ǒKX,iݤ T] #ʬ ~T G081u~5xG9*04e$o6l-$TF%=.ShaH2%v`!jC~\ k[wQ@tE0ialu' ~v$$TNŨ?.ZԬa 5G \Xc3y*;=d5)oF߲L @PQʅg/+N)O5"ZK R߃~ RD.,j`]X ksg0He{+Gu㛐ӻMLBLO#L|y9^! H gJ/4/-؆mPW TaVK~49'EIoXE}2φt/ !T;_*=CkѾ n2@@f5̃/ #ס#qu[9;:S?ҳIZ-uEh?Vj=ɕ5u]7<*U茀|')aWG؆ 6vguuhcw.MʵxebT[жwSF_xzruŸ Gj5ڭё?>45VEcXݑ;{jZ9G2z*}qf"ʟo] /}SHGAJ p^`oTj vj;jz@Obep:Ǿ ,œ؅߾+@@nF^zs'2# us>Vs%lhE  XYM²SﳔǜC,nL[:M{qhT&\>-~Vʠ5mٕarGm v4F3&TOE^<[̄7oSEU}J.Փ>ra)q.i0ҷG#؀6k Hy)MF2܈mCqV* WhOE*a[=<ܹM)RBfpWCjaُf.[R֖؎V Q oe݌:{?.$n77 ‚t(_,{Ϭ|?3)*@Qќ&Ag.D~X۝vc&BU'$-O .Ba[tj;ǽ*H>)еd^FMlm@7#؍͙Ј[!hPmKЉNJR(G^U2= g%m"Z G7iwKfyڣ }4kK &XeI Dvb1ZEc ?4?[[,jɞKvq6h_9[->旔ZeW-Qۄ܀,7?,XmݰB^+gŝP{,êEqK3WO.}ƂJ׿vEOBKm]XU4"\ C/?wh."XڂYWĂ%EƗ6Cx|FBNw2GqY`z}wW&X1@ܶW`\f$LZ&qU9} 5lh4xq!jVзuﯡU7`ҢԸQzȏ5\g?&<v`A6h (0T2(;WmobF :J]!.,2`ʖF~Є5 G'y{Fkg䇕iq8|I&%]ܽ܈v :" 7;S 6mrU 4"OW.I{pM` m-ųB_WG``<|gӢ/%^q+w#s9۵EZ|'{B \th眭~bO6X)ewZ'= ͼW~TΜEMs1g(U ~~KmѺÔD07ߴ;P Vx~pBBBiW6b`CT/Iɵjz};})t%HIO68ҡ a;/<_"fBynJdiU+z\ˢ#+!c%M-B`98aƦ`ᨁ.ZwAJ*N$J 'A|e00y`qffpah>ِF*Bp(t2,!)O+L@ F8.*"*T(B2c@#`t}x\E1E~*k!0ssP& +bSpMӤ;/yKV;9, .&8i2itj(ET%b[9%PĊ)(&`lRrE8f ku+6]ӂ V6dHࢹuޡlG~1\w8, 8ED*6Z.Va9 p>[@4FHQj9E.#V @0,Lp5r`iÆ2s 0νiEp\2 [n2BsXZP`#Ge Qc%ڹLH+paq=WrprcϟeO%'zALC0tńz #iUg1X`AsʝX ]NZZX<*e"W7!c"+K2'!&\ :Eie- q|D_ 6sM tbJEx,a*8%Cs U45]NZa-,WBt!Hj,>jFVMrs6Nݼý$AQ.aM,> 0p4(ZFDJf%Pj`U_-R.*sTvw-.?uэXwtRSaIU6. G4Ce 4+eVTT ri瑛NlƣD<="T8q8SW_ӂ M/D@*p;.U"XIܥU0R :\1ۺq=6^FXh,A`ӊMEXJuC6gP҂AUv2#U.4O3:J"lA *"X17^:xmٓ52,B\*=bWjDT1x̜0 LRihs_%J%UU6\L,6 (T]z\9loe%~ s(RP(V 78UFVbiy9dkI\ّ(y3Pܿ3L4b\:QU=0QN}w Un&Z*'9$<SP0h8XJH% d.=Y<e1N$dlapT09ǢfAy1A(\U=Ӌf% YY|,M*JE'0P TT*^`X.NJ$ZP #T\hL#br ^L8)TBZI<01xik^9'Jdb(X>X=8,րrL M {6xL[(UO/Kz#=D1}ly "#PXX:UAb8GGM& J( Xe=pIk^,EP Y @}V0~L QqXS{3b͹悢s:*-o"V5FK KX%,a KXR%,a)a KX%Y(!ea{E-,_4'^ au+[ ²=tiWX[ tT5V산4+M%@Xi7{iV82TX <Fi U8Vx9lX9q:J- Ã5K bs[[0XsD*>`csRw_1X_T`9%mg+"`efPCLeyDz Lkѥj'tgRL9P.x*̚**',u)YZ|=)Um?V6۔|"Y!X9~g׮[êIM sSp:)°fhS\ܚa=mn2m 1kXQ|azk"HA(;Pgn VflCǣeG2 28gn,-8M{#΍+8}QK.BKU0o u6.N3]>Oʨcyqe#@K5*N+d5p7;H3:Xf;}KTk2X-BKzL wMU"HCCbq\k ll1]^efph/b*a_] mo+b2դ\`^yw}߃:c@7v)3:T2O'Y,$kŦV4郰r3\JV9V\9ܠNcalÝ]r ĝ +`60 a7ɥw+ںNYX-OUr%,%a KX%,a K@XD ,a KXR?XIENDB`PNG  IHDRLP'HIDATx^k]U/::UI (b`LAy*Fh 7Aa4zW[# qi4򈢉$$$JUsk9z9מvIuܬ=:5|9^?6BmK`mp浀ko ,~8/a |{-F>&&4-F & )?)-#>8LkAiL6$r֤8&44 k8Q(A҈4ݶ7P:AX%1YL<=9SVĦidlpmQiP I#+@ /zqdy-6!\8& i@V /ih6aEtD"HHgnzSUx.fkA$B ۭB耷ő,\[`mBT1PG&uFPU3AŀXvnJFLL PM5B(C[Ȳ XQ* *G#"<߲R;`+E2.ck D멬(%A1$ EQٰ3 `C^zk P  )DX*q-QL{E,0ƑUGJ(d* 1> *3`eK6U xB~7C Y1-lkhQHH)źTSc:fKsT6*a}S (Ƽ5.TW1TgcdHxCɐ5 %5=14j zkH((UAi/AM1GEXpZ;4Ll)#7IT62VH2XȆ} Gx꣞4Dx8t {PJq\QR鱊XF>0%Ӟ[_sH92%`OQFQ R0%ӣdKN}`ţ+|/ CDco@1~GQ3+HxfC?]{k,,7.@T|3x^)J% *qdŊyϛN ~=x>qK ` yVX@Sgo1q46qQQ*d_aLQ{ٻXB܀X-0>7O8. F/s&{XQKjoq@.lL c1tphE(r]=h[KHp&0NO65yWLT.ЭG>@91N"a1@}c ߞO{#b!I= YOX{E+/oS~ӰyP&Ha$芀UHwfSݗ̙7 4cE}9j潨ߞ#V[a٨n$d}S(Yz (h6T( (9t:|o{& =!J hlE/+ŖHr)ZKʵp O2 t B (S!Vt_B8a =w.~@>Xhht"`Va7zzҰZڌg2$( 8C`iTV8ClO/TgiR !861-RsB#ZEeSN⑊Ua*"AȌz(/:=⽘Tp?T c%`>HQ+kz{m9ZˌVѣX,T):YHţ <Fc.OQiu6~+(EoqhOE,kQⒻ&,\m u8/IDoPeDԪ?*n@5cV}R, &ی^I J2 {;ruzoWxbUXQeXyNδ s" c=X1O/b.>{02>t> ң-jԸB6V@})-djmJ>*8,`)_"XϞݧM@'"-{Iˁ5_VVK;5ۤc{bq^ZLFʱ'@٨Z*Q:tX*e\ {CFOuG^* NΡ`N\.qX[!$}zFJ[_]=EcLw4QN\bƢUֿ/EW K밃n[ vB$IЬ$R{br XFpxW&ucnk=fsP!#TQ+7 <_4hE+JXhL|zʡ#j<*:.3l#-sll3)㛢Fa=O/ .hh\vv5~Xp^jH㢂~PXD@"u_F JUv}`󫨝t_@.^g颾CvΩ>C7!?jNK/r؏ZH-ih`鯆)v4Nx_x#Xo@%T"&Zu߫@Ԩ]1굳qߌn4ms>cE)Q^SKEi@ɂ ~eAB)}gYDU*۽ 'l (Gh j2UI,|1P*4jUYdL42uw6 o :!'Â+;x71~? \;1*2? äIX4yKYhX* *$*ErФ)w#ThW+ T*ٰ$4 QaGWv̽s V~+^Z2't4@F[ p~ЌBqQ/2򾍰U$` %-֋TڳBI " aEΣU|"7QkK֠ 4B:S}h_[>#T) 1T*F{Ѡ:+ XרB|G*LqEҟ9NÏv՟i_'fO?'e!S.H&( K:,[Սt O笊pX:Tm fcOnظ@H5B L/֔췏O!HVrWbSXQЮ W`t {1uUU8i^ϋLh?( ժ E_w#ދ솣~oxM=9®8._coJ4E|#f79 2J۠HB=ko)c*())BR;ocEpV6b$tk!zzzFD*,J,"\e{w;*NjkyТ-U*Kwx\9¢QO5Nb*;3b`uww焥w9Kz7QXm;i¢iKCg-X֭ FBV[ ,*71EOW܎Ra8'kq׷/sD 5,Z&11 wSh6f+ ;/Ds.~>r\n3!<+e+?k"I G E,v7;tnSxo Fsq 2ڔ{pˏ%x~+?0Sbp$ |Z!>?A?Uq4&/:e5o>9<{+rVoTX.s{_ŘQ@Fy,} S.D`Cwk1᱗sBt,kIgaQ:T t:z3UC wjhwXq$tNy><7Y)EZX eWlV^=b`3&"tU w,[g\VZ_Tdo#[fH%gm3úC кb3IXV1zk ^'od>r,Ɗ<jz&3-{`3EGnOo>ghok?W"X9,1IZDUo:kn‰ ,$_ĊvxB RXz/Ù]e3_Ā-YS r_&fVO>Cڅ8Iny뭷Vmƀi'Qw|BG}KXEҜb`?d^+ HEO0Ew6$,Z7TN `W@SFn4MKV!LSu98#+Zyp <Ł0+us)y ۞,&IN;O~M8mTڊ+ v[E+uxuWy oϼK4\|;agd'71L0+^?a :Z=9o1v#S:}â_rb_9d`mXAcY{ǿKZY0;%٧c^䩞 Z;[Ray4TxhRtVzNmz3\G3!Op*tFV3[||TdnuəféꍆQvowC $ Q^~!N؍hp)}{jvSfH*aq˧"Eja[Ԙ[*ʨpseúU4ZF ROGz6 /sT [5PoӸO“ֿ,CO/7O{[J`&p:ov,91GE )q )X|7>#ǚ=opAoykTr&}g!ݴ*``uV%"@Qd&@F/i*/1gRke)w6* ]1 o`5hOQu=}vӷh 'p ҢqOy%E-uJӗʍgD% Woo`M8{̕w&*>ES@ݞMo|WG*z◎9Zh,LYsX-yf.\Bƙn<}ea^_`o Ŏ[h))veBUqmo&X)ym}]s9;CO4JU a |[i{Nٸ*&M~'sxG)T:IΌV-j&r:&TX2`>X͢!-*-*`}ЄM.`>*]7xT·+7D@ّʱHel{aTUBo)0OjeEZ% ԣ/XK fϛ6~~~a$ TaLBr;J b{ d z3/~Xh^Kdojp90=4-&}:ez]٩`= GSP<%j TiOjS VA彇sr U8~FႨ>g(',X?_E}"9g Oz$Ā PυJG\vZ4od%da"`HţVE {\8;fEwzva kX,B:E/ޑ@iҞEcJ}CF 6=,V1Ba%GOQ:*E~@=pu9#r1X7)K@cfBȰG(E(Z_6G+ @ .ы&QJ}ڊ@"Ve30}09 ࠂ ,bܝs~cuUd JhFX+\ScE--a$@]EP7{c|0cZmM~f%\@ `Sk~$KRޕ@ C`H CXtҸ̚k@q\z}.~zD}`R`w>C;) yKp+E'Y?/=sV]H&(`HXѪX*jŧ" !KE$$&ȣE%gb>~Wv<|R8͖fސdF(Q&Q "P*yzr {^pܽ?v1q-bA 90*oJ:j"@٨tjqSJ%iRP Jɖۀݰs~GқI "X֦<1RtwGE٘@ *50`G/#D|L=hA%p4i[JxD JAѪ)6,L SP<,k;$X]I (zqT UH9/"O8^t5lXU(ռ#:ZJF. E#"h|&[iefXь^M Q^U S!YJȄJN'ov0iPe#?YwQ4,HSJ T`*2kn0j&pa +ыF KǵP٘~PJ@eGF)$g$++|[NRApEJ"`U!},A2J G~x=G6L# ̌^:59ŧt+#@ J)XQXoiE,^oƱS2iR/5ôeS+. =a`qdTԢHUFz ((B9G֐\!(2B}CF+"XW5(Ց9`"aOʰ0:W#k[hq&|BWU )`G>:`m,S&@!*52,kxӈZ``(PV`BuB-($Wj`Xո04wtP- ]& J%WamZ-C dfh6tm@ZHhuАV5kDs2$1. !¤ߠoAaR$2[0)P饌aq-[C&E D 0m4cXFhq m32̈1"4>fЃ*ĥ)d tH "Hà4*ê^L@clCJ?7% lNʼ=eXU>6zdsa#^Na'|a9 [pIENDB`PNG  IHDRLP(IDATx^ky'u{#!0@ lcs18(>؆@]%İYv %\16HI3^N3O[L='S:}f;tuUZ?bS 9FCY \Fa> sFa:&(e ckAdМ< #lV~PIBhKV7 Й4L58(IB$lm+GIl($()Hɘ@Kp&FaGE$$h}`,F$005 kQe0IHH Z`Ђa "o#kEPA##`5 +(D8~;@!ԊB ͇, ̏kr2xdra$6X4 X~\EzPd{  A}|XʂkV~T,Jy@0Tb,l_dyK, 4n`+F42.|+kVvTD)JF&D$* \fX 6%Fa@5P0AT۞@QE^Ku$G qL7 ƄK6 KlCHk]1ËVFZĢT@GĢS")BDP(BJcF) P#6}l/#\QiEPj̱T2HQBQĤD >f\U-hLE2!#Zj hdP>LoSCT4,BTEDlOcsc)=K3&pLU ztV xA(E&E k@aR`Qܠ R,"R0nq`12L6jAX'> U) aHeaQ1ⰠAE'ӖemԜ"ԚbGSX{eY%WxW+wO}# 1V.`lͽV@\-Y<ǡIo~x,,m{KxHF/ o˷0 on0$>Z7Ն^7) VB  (h S"PiW8 D:|cOq;QY8E43EfZ 4#.ZV$r S!=]ʂ{90G rN? |W;@5zq62 cQarq\9'XøG- +b*YSA:]@er`E"63hk;!Ӡ4)`Q{ɇOKȔ(WNH\5 p 'ܨE:T[LHEѪHce ,s2g%O>Tql)R5Rnhezד#E-D`,5zZaXMk+KArNz(R(ţ!2E3`v"ǥ)592מ0hƔުNlaŀ&a4)j.,R w" aE3;*Ia"H7d|)QC'N88ڴ@G UMڋoT&R JC( . LV mrʲs&/*V[L ,{=@㩻1:Ts&× *-~.v|k51pG㒋 i,qR*r *~(@etBƨKpqh ztx'~)`y"+]S#=MSko˵G"Q+:TGtRDcExmpQ}5V5s,>Tm_8wێ^gSLN`eL v:eǢ޲)RG:"gjO=x#Pvl RF2%S囖w"q cPqzzzs~ZUP6G@mW<}cD ˟ fO>?Ej8稔ECLp]toMPm7ft o0i P.%籞چhT":S,_b{^\25@oaq`,%Zڇt`H)bN)9`04e e Ņ"5UߺS#"`R'_bү3G-'r)8ɰ]xut֞cŁ=,jӡaБH2ZLħR(\uGD'zMO,ĉ%0(%*=}7ߥE@Fo^GL7寬ck uVSԂFHD+G z/Ye 0PeD-Ң5Mc *i XX6 Rj ]xk5wCZ) =T<%{֚4o:$XT_M@4hEjH݀J=0 ն\ @!Ɛ[.a=6No(j0Y\~]A|w'I}IH,%΢EK`4؁_ wJR Z},,sP $12j aѷQee.ez5ǹ;0ݗ'cl~9(%ڋz07,g,@F+T,Xc8*ӃGJK?RETgX;G6#KX\U q֩4i:+xS򭔈QC<X,jv"jMTFD4X.k*JHŦ 0GDplP5~[JAP̰vqXxpp=F"Ծg;ǠI{_ UT %oQgQ?Bej&^} `zx-#ްKik{[TI"LΧ}SUB=dVM5JW P_ 阾~ G *bɅ*/Kꄹ?x8]x ;;?S&O'6,FJ:+A]au Tv*FJz#p$g+4 F)?$Xu&!]L7)Ӟ܊߽O#uQfQk:6c1=,kx`W9W`'\2Z9 ؠuUeXzaw(ֈbP)X,j6~BU>y*/{/yp]k7bah )1/tl2p/ 5i WbYgPR 4݀U2:Κ5Ǣ_˶w~vsX"k_$A%T2,[TAep"X4`0݀*Wlz9+J ai*ދ]tj[Ѥ}Wx7ī:Kv2̟;}mġ"xϟ V"X'~Xi+- , Pl X,ުtPKsOCOo鱧aΊqκ]-01Pt,{MrkNx;|B`īGj+pޘE`) X"Q25I(zq܉G%̳1k>xa=a~J,j$,9wE;k k`X(,L+uP^Oxo"Ơu.}Y',C 2Rh.]U)%7bUxt~֕8w0,, 4KY W/ʉek BlXEy!v>n#P,>9JT UbV9a#ViqfXr/Qݿ;|W]Ϸܩt+@寳\gw>r9, R!AD wʦaUZV *q]ur(X_:h "TaѴCOrV*qo_ ((tf7̊^/R/ ;{OdX- %,z`Q-y:߃50>,eBoT83jZӹ'cX e B)<*sX-"VvXtkU6X kS}xVB]a} lщɧ - eH+mXY`)* 0B╸U􉈥QǘL/ulvhyy*?'-# r͚%jĒ17e/-OSp⯡P qJPJgaQ: 듃aFJ"`L&X&W!DC/3›ْ_?%u ]sn\{p| =20]m(`l%@v&%aq1I,:E%aSi XEL( [Uh/_FlD泸୯V+?,YS r_xire3r=Vw>G-lfe3[Ģ|4*GTup TOU4HUX+~Np C{ 7SzYK0&sO]/I0,Mz]'}X|66U(a}{h't-rWpQ/'˗m6GOǩ_>!Gi`gE6,7Q } y"L_ΏTh 7@fUQV#D|CߩuI_L*r}W)7Gw[ z+'(Ӫ)/Z ȔGxX/_ !{n!98O7g{{*'Am>XT5z ¥,.0\l6S,.(IX{@Pg'nd4@J> ~l-՞`Q?Ӯ<'͝)4Ж3>u  hsG_\ȁ#fKw'Aӑip6K]0MQ[4gx2s8@ WUt/y9OpG@þnq}*Ypמj"cb縷ƄJISOW"R=,^'RzE!S⽌VQ_D:OC><rsF4wLsUyySI*39,<[ ?J^\KN;.}EFh>)'QN`t>$f'TC$x>򤁋ő' 9Gu,:^"WgBY:iʓ}n86`UEWn6t3H)OcggI< ,"g8D%')Ctc>)F\5nHz4v 1ruYc~o~aT禣[@O'B)8eUF+X\P)GO%6B!tj’ݓohtCu8mݻP@]4jtG1"R|uز#au߸7%6HxQٱ$^$h<ِ,Z`US\ X]=`EZ$]-Å00(~YPK^x<5G`E/bXExȂ S.qPSH{14j*!T2ZOAQ) =jBPD ahef w:ӳp/rm%\yϿ#9K NzZH T2Xe ,*?,qӢDV~=&`HEQ"X->, wkݛN^؉7? E(fQs@%W$PiOcJ}}CFFfTQIPJLQDaȵej`aغb5$(9Nuv sRRE+zs3ZaG-Yp zhU5^>_mRE,MA!:wNHEO6tcw`7B}~W5;*dR~TD Hb,xA/+ OjLPԲ, I"PBХpGfU+tX%P0檆dZJPAG*;@,"jp@.~1\ RH%iP9\~v ڍ ?8^?.0b0oGj0`( ʎsE+?,㢩UL/ӢHIF2s/=O,j)NQ@؍*x5C+>-cOz 7"*/,yqITp:,F?"_Qң c0]OD.3R SѤ= O{^D)O/b[@TT"Z儕|Z(#8*KT˴ xڒ}l~Er픜9&** +?.R#>wʣǶ7"\! |[[ a #gO~;ٽ@5dېQeOapJUţBP3" ^|[.:=>&HοɁPuS"&R U8Z凕,}ԘhFWݏ,`nΚ? ϯyؓ$?]Kѡh1mUnX(k"" ?52X!bc۳D T ĕ4. bѪ) +lX",D yXWX9!H1/A8.nz*+c $zVOeXyA@HX $zdmţe HEшtgaXh/RTVLTvTaK,+%Ql* bH% >KdJL'֡aVFXG/aoIoTAx$(,d4H4ĺX㑉_ }sf c j%Xe^4&lʎ %QŠ-j,BJ ERInʺc&˜ycVS)R`bj,0*e PRhg=-¨8ZlVL(缪Pŀ,bCn-ؽKƿ[&3p և?z1\A(h23l5, G,V@s< <9 Z -!\9/BV"`.T(,}EPEL_2Ow~/*<5ЪZKŁE1Bf~h!\7NU[픑;-Z+,Bf84(ؾ#b 0YH V&pTrJFa "XT3h0>Ǧo/)A2 w ΅%EEwX Iodvω`eBꑡX 0! YTO`7 ZdFlKSXĄZPȜ^5႕ -CKS q_!dBz!A+`M]C#2O3Dl 4LUGK2d).M !wgP)XI0L1b#8Ad 9LTR`q!iDiiOJu 0A6DDi!Uq?Bp&E+)4f H!AQe)4H)yʟ^<*X[B

>IENDB`PNG  IHDR4EIDATx^g\]0 a(%U!dmVR YuUJ *PBȪRJB(%BHMɏI&ݪks94?x1qNS @XJX%,%,a KX%,a K KX%,a== l1$gjIa6խ;auV ݶlGSuVn < Į ۆJX k_ Rb.;@XLAiP +)5aoVjºMR]ֺXWPšT.10TXJ(6TXe*̄5M]y6(MV1hEnFiU&Z sk&vkBJkZΏahX19 n0G 6"nuJû3e+ 6f2Xn;P'ޝ,lb7betX'{Ƣȗw FXqI]{ af:ݱk4aM$AbՉ}sa]XdX15ұfJԨzwhl [a,VȻʼ7 @XEQXD{?]S2nfb&C>c9Y_$ƹݢs1nb7מjX`p ;5g]S,7ܗ{kTRKX$V[~wE,׊с\ˆKX<Ŵ/,S׆KXj r}n'Yzy-ӯ"Uhp <Մh3aeTIq- +^Ko+~z!a{{z%,a K@XD ,a KXR"%,a)KXOeceIENDB`PNG  IHDRLPPIDATx^ TՙNϫ=&>@ H0eFHn|֖̒5嚭XTԦ1ktA߂TPDWPT@7L'RʺVVy)$!s NDEXPB!;];' VVn$<,JPV86@q1H@,K8l# =`ޒ%Ep,|4T XnM>GIgRT׻e"RC@We`Q߭ݪςD$@~!q5s8k#/EHAs@#`]  }$WW&˾/4LlaO?(\C±`ϥc Kq\8\T{.9~sa8p\,ãW;"4,Bvy*B_ɩ8Tf7Tr~8o8 r8jL!ᒎUƵ2"$>ygܲ y*5 ׃W9c&ʌ gDQY٨&C`a Q񜊬K`*ǮN \4U"J±\I ֩LȋҥY.h qe"̛1L ,\KA;W\̽XzK7ޟ+QWS(\F>U|ެGd'wyJV(ԅA=P*9T}gmՋ?IPBXV@E(> R['gArՌǿӦ@̱kB,~\?Pȗ ,%[Iu,|w:~ He:$)*ҫ@Gs:!X2Pu`#BUx"0yAkz*;өx>e^ߣWb hwPtg6jA39ߡB$r; 1ccWDB*$%qO N}z11CͳO.Aگ#o n#gڝ.=3:Ty[X8u<\Z8`H\+Ե#"_yjF#F9L>Ʉi!N̄6w= L6 w:EՏŝB](t y Au\sqqB΢Ӝ].`'U:HY.Dު PCõ+e2ør/dx[9I?*ZfiB̏`ȭ{}}}NZL̍cptɸ_S9PyPjFJF TCp;* 5`ϾP ]x ۤuJ?C@ GW`R;=-kǓ`ʌ f2/Ns"j׆񻽈z[Qf1'q>S8Wsc\(BP%\6l6-ȏ7}Wh"8tzݻwOVcʌar%N0'O~,=Xu+r~:lù8bs_H:y{Qض{Mh,dIOB;y_#`cQ ͒K?TcqEÓYy:ٟWL40I1v`H!_R 9V4,NdO^\Q9_>Ċ%t,8So tA~I a"dځZɒֿuoÀܳ5{P훫G?I'5e?5p0b+ɤPg՞c~ųS1Xes;bS %ͱ"wPXR1X:vϡor,/d.&1ʼnW$תyy߅M pƲU[K8SDRK@TYAJ+?$ WacK|+y-(BEGJʭ%w{w}$T0k<` &WBTέz1+ q-`"RϯdS:Un{'IuwcܸqeU6477#8*WЭcbNM"+i dI QŸhx5IЏ!fىJvF.1r7MsCe~6ea!|_&{OD`/AX/I$a|gV^;tJD["bG!dXnJjX6ZV>o3yTE;$JX >\f"e0R(%RU6ex'Cµ \p}v&p.Kv!ъƯXvZVP*էuغbicٞ#;uW# L= c%WKHr;.U($yjkHѐs.׵hMKJ:_XoAm(RV!ڋ{T\lCZQ0q{ b BTN3ʲ<R)Ҟpczs=m:_s%ŎT}n3j׬XD.}m\2ȹCs*wC!`}쎥xWe759<2p=ulv8Kn$w4ȀJZ|H\;Wς[.C/K]$>!0VAWԴ>m5ҹAx XظVx4_ T.]k[2J[g| WPʣ^dŒ #=\-HE!9@Ǝ }Xz  P4ݥD^-8jr{s=2uxB %d 侚^nһ>^ҩ$ƹ3u/]'A &,,4ZwEWwA2ε$avaew`)aCrx :|B޽|& \qx/c W?2kvJF`X*+]0 )WRʃwl}{TЧWSR>-2(J<-$D0i$<0%J ~ ʴ6 Ğ_*lPX=A.z(_3)^u2 yU탥w1T46|C%(EӸT.j,.` (NfCA[U'"" |S󊼸j|ȫܡ,!݌WIO c *<92,Lr ] eS `=d(Gt`^B & Z`å#~|y5QwM IENDB`PNG  IHDRLPIDATx^ \y{vdz5cb%i )$`!i>Q/BQ[)*i)MZ6%mQIF&8DQmlCjb}ܞbi{?ߞ]=4ow3~/I*(,5|@; 5:yGp08P}[V?Ql9X@D$T VCV@AR(@QGq,K;[D`\`Pt=;iGiI TeD%!"0$]+@ȽJH.F`-^q$q pϔt"iU]H.N%݃$* kV8Pϛ *CUNZ9X$i "(%:*Fflٴ@H +T,KXBhTdy&bEO _.IE 5ؼagڟk0(s9^q]%|>PW?lo- ĈC~z:yplB]JCg-To?} * F;Fs,'}s"BMY FVV*29r ֨`\޾$Bj$؁%YĵXUPI'K?)mx^" V&ky ۯF0#o(f<(`k XY + ykP5}Rb8IʼAUBD@wlIaiM G ' <+RH YU0UwkaBuj^*q,M\ہo'0ф@B`" XN֡v~t܁ BÒ/f*ܭNK֙Ý g(,Tt$8+4!Ԕr,)>pBv'׹+Ѵ5k("`P.  =;wgu/bD(&7z_.Ց-z6A_@UJNR-JM(,9e]t0m}iT`jԁb* *?L%{'|bAp~C~8myiXj^XV<*_Nͭ478|:\j{/{ mCݨ[PVDUf#LBl  Q Ӛhq ]+Ե#}6&QA"PX-[.{ ]z AߏE~坟r y+?r. R)&+vg/,0e }CCXƎ*S,T|`Op8/2b1Fc1^'?( *R"w|)';u"E u0MhR)Ҟܒpcڏ|5T1$"ŚaPfg6"pk[$碐%۰@"\ cX$WB̷KqVe59窕oV8P$%!0VAWԴZ!m5ҹAooVHhԻ{-.[#owf&iW#"?±bmp*q5-Ew]ww1"Z_y+P) d\^C%X\0i$<0%J aҬ   ZĞ_*nZX5A.,PfR/d(,b>he J>%Pq6 `qQd;g ڪ:9XN3گȋY/(0(Kb2Z\% >I̫Zd| 9X{5y!I19X<nH U9}6.\ ;iGĊ%yd{IENDB`PNG  IHDR4EIDATx^ggC(e0eQ(ir(#PePJ RB)F*e4B.;푓,99^nv{r7y.)??xy~zS ,a)a KX%,a KXJX%,%,a KX]4uru.݃] +ݡ|͑&;V nD\X8GyX߁ +PwE0b*:b*kLX1<F xŚFEXթ`'ՈV ^x:JcD[uoXWjX: `դ#}U |5&5`T8 3|jTF͘ A}@뒝gPs0HS8D D3gw4p-X69ZR w`]j6%cե9XWLT,KSy²SuN NB|UNUZ`٩~uezGU~9*)SU6Xv̰V(fl<_eՁ86!fU}B`㨭̰*z~ us=>G Bj,?KcFޗB#"*{C~&maFv #O$qD ,a KXR"%,a)KX%,a KX29IENDB`PNG  IHDRLP(cIDATx^i^U.wirrґĐ JH@iWWn "(9u*[UNhB0,Q,)T"" oCҐ4'IN}{};s}''9|5;yƜsZ1k  10kùj fh{38vL/ dl"&(;?z O#` pLD䇣:ރwz-cD ㇧ ed`4& I#㙸E:Nc{4* ܀vGg֖ Fh`FX EL~HD01 46B#r pI%JcrCD Pu#Cj44մaUB.a T>P SFHDda]ָ.޺nQΏ ̃lOghX$Qr6"FSt#6BS2S0 *+:٘jA⸡w'{g+¨q@Fd} #&OZʎR*BYɎL)2.U:. I0DClf!#sF0h)S22QRQɾ&*BK@".fY\lh Y&`=\@`\\iNa"$`4(djB1Ej`d72` PTy*3d/2ކq^lp,=uQF&z4e kCNGT9'NʕVZXUTs0218b=M!:qOW_âp Q(%*BFD .P RqWPEcmpViFPC0A%ze G>LE# rL}EQJzTGF)Lr8NY Z!D)\) ?.Xj'rRƔ[=XT < TP^*5:''֙N[krEzCzF(i)L D'I#W%kT +JRcƞOVZVt?*TGA(%MؙI "5Ǽ/\ * XrE뛦{K<DNT&M[)yXs#rNT&.qY ԽB" cqĪZ} k :\ˇ3F)]O٩Qyt"DhWx Ȼay#QT4& 0FHIdj\KVk9/7._XMzaAȔ$Q☠4rG*b 2bJBhf"8jʉC.>1fJ. VvPUz#&␾.j <qPɈeG+ 1XDtQٸ8fZ;\0<ӈBOwdM}%a#4\U*dip)N版TFHXZ_`JrQ`Ϣ> rD{N@Rl\ Q{ cZB}O%ZQ,NCB6ʷ Q?S_#Hkl q!FUƢ MWÿǫF{p"JQ+ >`#.rҗa¢J{i{ӛB2 1,=zʆCb/4.^c;^[Ʊ;?ӗrF$`bOX9Rc v{܁.Yo`s} fԢ{[OTE {E3='DZx1>cp2~F⾔$X7___(%%t޲[XfP&,Qe){P{""i*bU|_LJwX~"K8 [ 1)\Ѹa>eBrp"܅ ?L}]V7BYTcT)@np1tUI1~2;__ŚL@%pMEa8~6\+~0(O:j 9ƧCM3PvcjϢkT:"l8O'`UPE%bғ0=pޚ}] ',q$nLAêy_Sq0* 'j5\"X:aK(VrQ/L}$4$Ո䚠5g5^n_^?HfW\i͘n}] Q˕U.2#S[c稲"xL,ĕnu%ǜSa?? 3^O QYǣ\8( xtwq%cZNveuף%:z\J:X4 >^n}k܀'$u;,BF ~(Lu}s=;8ؑK1嫳0l3^eA}Z]>쯩Kɡ||ӟN.,W H\"]OBk;bE|1ܵ|||C ݋t(a}݌TSDu5OELB]ʈ)^ƼBKj> 릗%xIT@qٷp1N92\(t(c , ^(Pd#@`׌XfL~lG /b7E(Иx^i9Qލ0J`e}:օf R Ĵc LUeiXyRUǎn*:`C ie@E(^_<LDxg|\D,2,%ZRkqRO}!!FW=FI'90 kaͲTH5 Zpqզ=X7=aaR!ARsQNL`hG)=NXզB{ & O Ư;[T|W۷ă@ÊQmiGǸ,ֵcp UwBɞUT LF<ӞNUOVq ="QG٘?P]Gb90 ,[p11ωO܈[T~GYlF*JT !)T8"cĊQ*rz\.תzy+lŊ5x';dArޙWc J)Sa>f RѪ*UO 0 lujHFX;fcJ}~@U:jA 0WctT+clD* QϧVzoS@vjDP)_S)LVKъLΚ*J5Q 'h~F\ GDjԵ:mPvsѲL\Mp~e3hrLSE⼕#Z}q>@%g6=+*PIB'! VagZ_WzLYzwd`i?>Vհ:R5V.F*Up= /HO|=*rzxA0ɨςW+kp/[ Vb}]ceWTg}%) =c DڴGR$S|鯾>* . _$%;mPr tg^ҳ4),V` *UDAWhv$؈߱sSkBEۄ5 /ObZW{@(: * |r|Oޢ&KX$׀[aWP2gӠ#X{n AՃ aNA!aKbTl` .,Nwha)XV%j+p'.F,\ jByA'fXV|fK9WӰ}5TB13*$tG\TK^˖J<2f E{=V}\_a n|*ubbPĵFYV jҕ3O ]fa4m&$[VX`*P) R0u~Ν;{ ¾@SSdAO`?ߎg\)ܫ6Nl9𡡏 +jj}@k cʊjذaF۹ #|^TEL\EjV=rBQ;JL=%T%րDu#Xx SaҠzVÙ(3 g\7d Kھ};aժާR5+45u]_6AeelBp U Pݎ-x성t bX& yLPj1qb"tN:*]~f UWc=Lj;{ಠ.zLh3fGjSZBV4GI]߹Bs$&ׄ( dm%F\_c eZL2B`$?|Vl*YPI:<<6^B1ʻ yiC]9 u12t>˲.nn⪤à4dDpҟϿVJA+䩯JWi `Amy(0KvөOzL1B|gC,jQƾ}.f/ f^PBZ}eAjêF-?7/r)dFbq gF.;gEXVJԨtXv"BO~LLr_Vip[X mi@&F+LDd NȲ;/x *XqnwWϺӠzYzPO2X` XvF,B wr*Ղ5ßQP]BP`U(R^9w46Рjr5xHZ:¿-qyc"$߹c|S vtOmKY7l}xK4HL DM\,s}Oja1rVZ:^G- n9ظq<1s,f8Asx Ў*ч 0I}OǍuWb>㩫ٞ}H LN7p&Kvx`AaȗZpΌWntL^DZ\Sp g6l kӰ~6^Br>zapnt8`^A :D+th[j4j G>D]3u1jAb,L1)x+gj֭[& ;+ш잭}OCVlSaӿDx$&`[W`#jwzZ%p7TLj +~ hW WQٰ=4fQʏ""v؍]'1"d~{NIPم];hg? Wجƴg 5Qa=:W'Urb tL5 5XgHۧ^_`- {ƒ#Nr!j* o 㓙xFRD3#S Y{4:tZt۽ọћvkqbJ,Mf [Nq=ׯ_/;6`ݻ~{`S =X4#mF@\y|)\v܈ ϨY]:# le31qyU9:E>81Yv>{7aiTkW_x^>9XZXB]K .oj$`܉ӯmH41,osquuu n'WO+ODh%|JZ-.0͉OذߌjB(\rYpEXZ?K{Aj,wJD ΋f' k<a%S-(P ˊTjӏARAJD%gDwV"Qifܝz=H>X"^Im`=O~pJb☈R4Xm"a X ~D:-tĒz+%a0rU ߊ鋰a)؂ v(ƴ]Fթ]͘1IsX'NXSPdfT'G`,H Q,}9SWY(֣S >iP`JS1/_l6(e"鏸STsܰbQY&"PQ 3%ыrYoIzjYJA T5)~Œ~?8wB}zQpwsZ::<*ju0z1-SGaFs _ /Mia JZ9+㐨aC[p>N\1Y;uԕ K`8JBld ώ2=!'HIme,؍@C>K[wn;pZ]DW Vh%wHơ}  >*~wݍ'}8"TªGJ}wFDXŔ^K`ޮƠ(b#J0)LP=VQ+g 5Us8rz?a1ѨI"0}*soQ*$JXj{%nk3r֒C{#\VbשּׂKEτbT\M?\2#^g/%~؎r Grb=ezcA>9ʗ iTJ>\uQ2`vX:K`儀yX?9QوhUm'DHX qJ W宻RS%HJT=訥qiXy,cS]UTFr宯7oz4p jE-S. ؼT.z K0VPBX|}&T N_9 ׁ3DO蕂%xJ'1q8S:.0F>c 'Z4ujC/%ׂJE-p5ƽ]w1zexjFLr**EH,oPK쉭!2Ѹ9ܾ@xp1T7L)t@ɨj/ T@E/kSU6b J! 7JT ,T u1tX 1j|'dPú"/je C" J_c\D LG-F,±@"!S 'vaK* zXV~Xp_X:zVO4'2TCg7A@YO^PUcL{',iXe^=-P28xFǦqYk?(F)?*}K|*HG-?.{ B@AΆV`d~/@Ȃw?) `}qL܂q?n-3"Z*_ciștVviҚ9,µ !LŊ4uݦF0)j+ q d Li,ǦӲ劳N@R*_+ nqX)LjQ6U$Uä?ye+T0.{d+CWL\'Ue+<,]E@T`3p:ͪ],cR5P*AEw@e Y@S 3H[`bP:\+%* 92p9>O"SLJJP_-4rxe STܤͅ#cl :@䐲e2L P͋9<9 a"TV4Gs|jdhFts,c!!aD @ XrC"L֮k؎р?B%IENDB`PNG  IHDRLP(IDATx^kfE&|VfeݠXrW QDGitlhiBhAml]VanfPnU]nEaPPԕ$EV޿sb2^⋓嗗X묈s~PgO|q"c vuvp vQ3nX3%%03 nXhp`; fnXS#ʏ$ ݰ0&FpBm}|{Cvj(ĐP~<9#c$*Ř#YEF0:YŨaCb@}H|9V14FTawJbP!P*0"˟*2]5ɨCrP>PXa]bch,Ŧr!`M2(u Pޣ)6X4`XS U aAVE 3N 4FAZZ*RH@ 2A$σFPplT(*.;FuK E>CUg.,p)[txT%\C?JIaY0vD3t7+2[=hLQֆ CpE(""Q&U .JaMyX{X +*Mx 2b{ыS#OO(j L"XUkU ItriRػ"0H04}^DO4ML:JFcTSAeBf s ͂'\Z`Xf #8,{:a ^0laPzU{X-sBeOY=+!sȼqYhfpi5UZ тBu`.F69S^sQE $AX(;v0;. $>"g GFzE9/U/j5֙Ȃp⠤>2)J>g(K"dOD]L۱3UWԋ^A\)0,.'VT0J©O1 P(A(:@:̏X^r  `*yǥJ0;Ka5>&iEY?(N}7ry2(\L ҈e{ƌK@iJd :M8Ub#?5xk]j&eAS%WP_v媥O} *ǃsT>'pMzSg|d?l>z/Y "l0|PAQAEzz+P ,ZMf*V;R@&xS)J ^01.p%$cBHK?} H~xE$% m@69x 3:dz+,@X#ȬlZ*;4)đ9DG~CT\K>8G!GxDŽ2@t2rZ?ބ7ލ9-Sb jIH+.)aゃQ `T)zH{K@98KA`)"SrРEy?sΡx;aN7[Ys3b"\{S98uWV}X<S3,6]jqOiσ$c{%p(-? ¨U\*EgTHWP GzBX8s81Ȱ/^JRc,E%8{`rm8}j zrDQai!ZKVj0~/!Ty+aX .&^qQͥiK6ׂu/wѨ I hj .\؆ Ͼt#.XZ!Y um1Yݔ]˗rdX PM@6j3Д8r8&YV1̰ykx$ !RT|6\޻oFU{i;~ahRTmnuW#k4,݌-@?2ՄxQ9P5QUCS"X~|ӱ8PpoQhe{4h YR7֙{nځ>- THU) c .Auj<CՉ7¾̓x"J@:ka-3s,*K-"0NT)X3X3*pIu4#nj洡f뷰U/cwW^:Ge(_э/>ƘG,FMCH,^薽Q۟ pO0=Gq4b}VPa`s/a-7(aL:QׁiU(`,3E[ xR U) KҟCEC@ơŗe{#L<S#Zjl4LB`٫X;9Z^=Z#8YҰ||J(Q>dh"X*,F#={N>wi"^J^g ځ oMZW߅P =PF&Zs_ٞbl{8 :w,7`Q hEo2V5욨l,i}Ռv'2bM>h}; 2  Ƒ*aXAXW< N,iWVW9+N~jհ'bDMQ}qI(!ӰU0(.e7H |<`y[򗧶]@jX妉 V`gPIJE *$i~ׂ|Vc{}pE)*hE;x t*^_5e0XTT{vq9ΜU_; +&v城JUt!~)PM33ZMNmbinXT@QR`( W2ŝe$0*?3h=WOZ8@%ς[A_LXyר`\HZUJ UpjcrZsxJ J1i!BVrAE--{3j, /aV@.k1&#AMPz#iq\s3+3Ac*d]*հU47~W TxV& P 7Ah5aUl!kqXQFk}a; QH!\XheիW4k+VShkM}XEMIcQ} ]Y $)kF&T`n3KŰ:ZDZ~1kK2\hK*Ni-0)ZM Xv\7 PޗDP /Æ:X4CU>wf˨x $Ɵehe6UQp-"^ݠ\ ହ-/[F (*KaO>)TxlyW>,FŋMP毼hUBJ_LUTqXЯz,*tGpEvӋ~G2,[ͩIQW)DP=QaIa+@^C H(Z^S_¨S /M&X4Z W n?U`#UV^jES@a 1TQT B,={ދw?/]w~4΢&m:RVSݐ%_ޔChM>0:SC~gz\}$Ç(xTIBG׮6b՚fr}N{t0dSU|S>XUTZ LyzX|A80xJ<*=5 c~.t(cpE2.d(*~t.Wk k*̳!HhgdZv#X՗${ǤTb֘~=jWwIFp$[T ^f% A{+)ZMiT?V2EgTzMva`稀"XRcS7^ QT XTtCH wS x}oz|>ok#Hh#>j!+lnQ]zjrH]_v"=XWM?E-\:L*8{N>`'tW%^E6Qt0$k1HA:+'w'=r,YIWNy6=Fz=X69T0HP1;h}{{+*+/ rZx+#ރ@"d Kv]z*ީ3FP݋E)Pq:<{JIP5 s)iXHmA҈W@&VკU׺q\݉{GP9. @"Xr%)c+o N,&x2:drh6FQ c{k7;,!m&1HcaPwVv}TLTW͡ >QTpClt(=E-O/(uN)'h s'~MG(eGGbqdզ!:Znv3ẊFXp.itu; @CBUR\*tx!˥#Ψ3nO1@~ +؇!K3KiOzKh%4j51.Bu|bQG1n;6YT(ťBzwP=z ,;kºIŠK0Q:NX; CRaӿ4jURgJbƹ{Ǐ8VH|pvU PH՛µ-\둍xop :]QyoZ_MXg.d5q(tH(- 0I5s:Y؎j;݁{d{O8.iJ3,<v蟫;E+BE+Fia=\e=SӡD/F%F/Ev'E8 DV{ ƒ(8:d6? Q7ogfN4N)ыb-b6 wJjӟ&> '⨋aV*cRMxo@4LL5!y75?۞r^DUb` rmuPmY VP4b`N\?S*rz5m ڑxE|pGUA!1X ,6,[+ \ *\cNhBqA]ثV!)ւ'I.U(R )a}tPΈưDPs.Gߚ+ml}DSl6Z3|jV`݀!Xw3~w7j Ǥcp%EQIpX,}iѳȚPҡ|h^F C#W+?wbHP)Nk,.}紗ymך>E>'౯߂+ Nڬ֛jp03`mQ @)QLDpyV^kËnǥŰX,mo?8 疊1ހ7((Z(KC;ǩ}i6"% /z9\Zoو³Vbl0e8T4h!.|9Wƕ< @q^[Pȧw)*K'Gg,?I E!$-R%#E. |q.v$(Q#w;L yMj+Em(ju 8p5!`ŵgBbDEa &A'O4$3YzYKE8OSA'qsaT ȋT.J5! +(3a}ִ҆X\Jl:n M2V\{u(TaÒ(cZpm]<ǹǣ)xW!܁aT `IOQJƼ(K%,6Kz$kM-H<\uȧJaQ}4}[P4,,mx_ۑ @0*)oLkjsrW7KE-J1\K.iTa٦*HZP#hK^!4HW$%Q)HB!:lŸ5) }(21*gP|zWް7. ()Xp7*\] 滄AmbbBբc@8g,"'=Sb8r5x*+&o̘2ΝSa(\^ZJzEAPN3MT*Wwi5 8 ^P!.jI_CEc>Ow;xAĥѫ(5La*L$ > @1$ʂF}(2LLYAECSJ0.\8-e$xʍJ{%P E) A%) AÚF3e/ $U/j^\kﰕ STsi)L=\.ՌR0hFE`4NFjFQ @^=Ni ?! &GL,TM"BꝌJ%ɴu)UC\^OQ+K襀ߧK)(2FIq)8FXE,ぢ$( iph.*aW`~RP:ޡQ`M{E@絢BCFt,[T0 0*r8Vz[@Ric\Iz{U+i @"Q+  d Ni+id LڏB5Ⰻ$AR V W=`0^QXqtڈ``L< G)yNg|Xƃ+ LŁ dgSȥ=E'yRU0ZI?}a]b aX\N]acpSdQX3&{)6G)!T3%Z1,q\q`G 6Dn IzPtAE>ny\c|c?21& J3a1.Bд)8B^q`6SÂI(Zq# CBG) m)/&G)0*Xj&-1 3G" 7FĐB1AEQ|Xqd F"Ębv m[]74~H)vÚ8G8xQnHRa5>!A52bvÚ&0? " k#8ڤ`w+HIENDB`PNG  IHDR4EIDATx^oU閂* C$M$(! Ej h-V w/1&b+mRvNnf9lgL53'ܝʹ'w<$O ˰,2,2,2,˰ ˰ ˰ 2,2,2,ò ˰ ˰ ˰,2,j1v +45M<)Уnba5.i9Ê$MtbC6z@团}Y:)*!X@r,֎ c1n 9S(S>ԋu>XP`__dXL:XKUc.JD h:KTz*+Z~Aer\oST)]X{kD̅pgRU t_.]a -Ֆ SڅLQ#N]TkINu ku ,^ SU[)񄮁6 P%%5[1Ub XݺgT[p\whNh*nUT2UaATVR>jݹxE$UCh"~JQ}T5X)ciӿkAZkU\)!Ol5Sy9Y҂HZs S)e*ʩG!,9wx6Uư.DP}g.0]OFQ]źdo_݌` wk_9|\%;+XWt?3'X_5q W^ua>"}2'U$>~a?@ţ8i:@oCtqPO{;{  xOE`gA~U3ǸǷ(qWHoepelD0ހƃrWa@ٌ )p^Mt|= i'0w~|ϡw6$"jڍW(H,م㽳a +:/{gSUv[.zQQDul|cZ:8j43~tT CAq ӱMjLiC!ZSTh$Q0m2t*BLQRtN?E&uqs6qJN'cmSJV /9FĴ簾j>}^r=YCCCٖ-[o߾\fACKyl3vΜ9iAAA\m5<|( yqm޼wKyyyȨ7`Y) ^^VnўÇg 2-vҥҙ3gĉm |;F张<3fܷoV",Yhf`c& #~+VS)5kAo"/tnޏpQF/.]@eEy/@˃;~NT0O9>\[[Hdbޏ 6~<#pCG:$J{?^zćmV6Lߣ:2l|:Gs[.M<<|Z ׮]<OD߼ x?Vz]cVZOCh _^o)u0 2O>bȮkbSO& ,(cè /A%k?{ahZr$'T"+@iҤ>U`<v64xdx֬Y:tT&V%?"7o.twBӐF/Νx22̬%_ v9ظyРAsOO'=^Z^y`y䳯?G{BԚ.Ջ??eW"%@alx<Hn7t>ǔ<˞& gLI (x?˰ @kkk7D@([,K!V?r7 W30chaDRԆijZ7*taݾJ0ΟbJ=5`x@3 ~MBHߐ>Q0Πl\zy.H< / o '9e. (Q Vk=O|r~u:b @+BKà'b>LxHr@ XǏՁ4ch`j.@'8QߴbcOǸVJy?DGGqGql=Sڰ  |MXt3O>= U_A4QqA~%v^iii#φrNcѿzfT{6C }v,R)8$'z{)X7mh{K-7775%%3T[)c,Dg"J4k8HT@Lw>vtI;)VZWG%]%oڰ/[Naݔ$9mt~ܹ &R,)US=?٘%-ӱ@\RRR4A0QE$k {bSpDT;:GnBGDi/PTJjm#""` ٓ~5ӧbq݀uÂ.5]r5wm*8́ <#vMOl43tg={jȰt{q~ ɱ[{CY]ˀ-Gv8#Z1|ذaoMڈrhdցi.0)S Kz`DB#WTT,^Nc*x#&s '*h |,iPQ K;1 A!nCNW?T@;boFyֲeˎUSw] .q.<L81 Ԕ#G&3gΔ @x w) yM"eȎ; 233, :_V() 6R$,,l=… Gpu!;kV_{?/8N0wvv1Ná{^@u\9reBF@P`V;re?deeBqcb8R!T F<ːKxGkc222N蒁 B Fa湳N8ʏA,㘦e?/* /5si k2m{k` 47qwwsaIu76o-Ci-`004>S]C Ql\R@ eG/14J/Cq,|)|GHj  QD&qB@8F/@ ``t |pxp >VS0 >qjTӔc cc8@26_@K݀@/#dՎR=>jBT_Gk zt)o$OdP!Ĉ{8p(L(0> |)!W#k'%z| 2$7/Zw3kLpamz e K0@F1@%hRz/@ȢL` @_$ֱ_C:n?)gIENDB`PNG  IHDRxuouIDATx^j1Pۉ}.$0h Ý՞Og8g/ϮQG7y!@0L#H`phDc N ؿ4b{Ay齛"h إAi1>xH?@3qV"ئѮxF"ujl}o )t[ޫ!o =Dɦ= 0(cA"Ye\|6: xeF8`b&KsrLcް_ΐ.^HB|,.q} a4Bgp֯ <@'R&!\gƈ*.&QE}< eE8sNNB#4>'ز@؁Dd oN>3m{?F:a0Zx{B+:д&"n@(ŧQkx/Dz#xEx `":$]scg# o`ķQclnT[F-+A" Qry&@i1o@@.eoYǯH[#0/s%ƄA_N>'HAB$@ v]FK7H?BwJ9|豛θ*?k" RcK?52Ju>z bAuز!HeFp//GϺeo vɗ s2J#\\|Dp&<ԟJFI苉~ޙUmxY<6&['1 PUCiABٚ Z(D[h h%ġ@meƞǓ]>=7ѹ?/ϻ+v V c/a5t tYzwLd-'T\PY1\V7. B<{G>t4oMua޽V:XdSr1XUЙQd!4aX4|nֶ= {٭Xd!gS4dܐ%dOOt>=23^ތ?~8cp}یO;H+ i"Ivtp|!Y(Ɠga-csQ/zuRݯBϲ\ T4O;Or=%~#MX^s&f|u!&_x }?x-{$XK TB B auӫ]= !0Q_Q_ kxVtߴ ֪ai H%+]].oƔWbV𗨁߻ 8$gD>bCp=Ipߞ/= Kws_3@uue'[3 ~ᓫ\"xĂ3@R:鹘̒C_RhD1Yՠ+˳>Ffر}4R V W:rYHbGH"p]PV&dm]Ȝr?wCƌ*QN9_tl}J40՞ANVNRY 8p,<'!.蕯{7&!׋n7pEχQ nHOe2~zgs>>U͘1WA@ֵn#*8.aX9OC.kNj[V(ywA}{Һ.n(jBP (s^"WeKt—?@ߩ`V184zE^=;s>< 9p\C_ZGNgX 2ja%I Uc|5.lrU^^O1Nl1VBA j!jUoXo|Lyv`@.B19rA0,(^8z5-,>m.z}+ jG{\0P/ 3$q7NäF |%|@hغ $"'e SB~z~㩘f9cۿ?U|| =6PC y ~Uy >o#_? $mߋ_a Tz>%EHمO8 s|cJt݌ [CCC~QBapS7&][2z!Y B.@HU)vN#2,s6JJnN > t\;4}>"Dih ̻+<[2n'W?w09ڎtCZ5z4}mꑷ>ҡPavW O^%/Bae7_%f}@h7 @ н q+EXq9 FiŐ^؉Ms@} AoDOQAXa300 *L~סMiD(NftfWT3ϣXd]~g=k a#PłTn|Ϸv>ȹ܊z6ϭ"U^ƋP(6Ez Zѳ`979 'P=, Z}-_n:>hՃ|#zHpEY/”c>b#S;@uuuqd8sKM%ܖ!$#wkNT AhNJ|3EGЛطj-,hinnŇoʷ\g̃M^xfPXs>쇰=++_#C5 Fp WiL5վN-FoxIob2:|z%9~KX c$Fג 4}B a3[x^Csq}PvAw'ϴk [ U0¬'aj9'aVSWJCs`+aBk  coSe44f7iW? t7Z1z |#Ӫ]RVx$rB-$ΠÈu.46VGO  b~䞠!d1|e3Q{|Dr~C$&_|r; è7PsC[ͺ_eL=hzO {}w;L s>.84`) ݐ]PEB-4+Nz. !YHt7WOv@Q rŻ!]P. :(JxJإJ,!|NfvcǪ0Eq9?N?J 2Cۀ sp TxÄsofOֱTm{ϧKAحe_ʅ=B1fhx oA Ǡ*3'4Sk7O͌./U"2 mzY+X#16E!> #4 .V3ityOE_<(ӐD .!2Hq lϦ2Dr3y.0%=clHw`gR`;>:QsFy1 T(aHÝwUyX}tPM J ,#| 9 ( Hb27ü_%S^jIENDB`PNG  IHDRxuoIDATx^ъ0FxwWiK_0n ^|cxX3y־?35̚|g*wz^M?< 8߳pսB1@Ǵ%Y(19@OW1j:.[3R߯7XᱯkBAYú:7#loP;] OQ;<(3$yVa 8 БY% .}$S_X'r1 T#~!:%{ ܇='$l+_rg0v`Z__XPZ_aYc1z ' aޓQ/$݆{H|g2a|l9A'l% Qn@֟Vڨux/ULb8)6ψ DpuD#IҡsxH,/4a$cUt".72D\PbU|SL0 ЗӰy<|SֳObH|va|Г/ܳW!)pБK|t^2?+IeX?\+@A_ Q 0v}\CK~]YяDI¤;av\= Z zq z$"~wwBH2~4z"2R9t4xz"<'{GK.$p|':Q:eī_>2:n O9 *B&_L GM~ca\c(8\9gtݶmi-@% (` hTD@x QHA!Q" ( (V7l+-eo6osal;;g_(dbك_s@WPZYg 儌tfaC.%2`F|@H[?}XO'ASxX~ |0y aCvc7E l3#22z?>1Y<UZ8qu .=A*x OI\5UjLƆ^Sh>i#|,= $ׁ8&lmhpTx;{1 G,O7Z5@*fjGnG̨g)__E3FF\ɰbDbǻ ց1L>hY)/i^aTp`.X}{ ;%tR] ض Pcɜ>iG<'v{W l»_wK(B()R/H0`!X?>+^rhBßb^  t@tцbf Is-.%,XLQvp<ͽ% &S t1J#;g&})^€힡[G4HA#60@=dFY n4~n%!y,̸I"QUM @F43OvEip8Ř )h! `jE 'ɝ8t׫0,= ߎHcP<ײ;?gR6|BCȬB ԃ6 )FZ4@vp =I,=},%B& H2"+ÿ]<`)DTC'~|FtHH(y Wx1bl|[B "K!|Xь $Jk ..B҆PzR:hY54J yK_H!_1Qj #t"ɫ|9$qۊ#&hl̻a)fA !@Nf-zyFt[q44^&)+g[  `@lw޾ou/z/@T #-D0q(T_)X>"|%XR=b )&ҥF(EOa#21H< s+1cFT"&4%n <m$GIcIfJGkL &ec@zH%NCOxp.  Q2)aD@y$G'=Tf bh#:#:2+m?Lih{r,Qhb`¿շ^W a:}LQ+F΢HzUk@B ! NVs`9jkELb 2tj6 $T|4p,G˩>gy]Ug\2m pބwJF +]E$>?φ6 4xPvR|i,X|fѬR1vɱÎbEΥW j<\PߒBhP![Ωr`188\93PHӒͺGx3WV?2cBjbmI|8Va4Q$qz&З7q$cb˂`9GG홼-<8Bg/]X GqƪY2S+5Im A԰+Wm9K z]ր B.ǜ:Ѯ U L;G̗Oց`r\Ih RuT dVp)|>kY 򷥳ϛ\4F2*&,YK}pf{8vkp;_pjoL&'B2])(7l.9޽5, KwYUsOZyLҞBHG-9-7H!6vc-v6@Qd-9r`A7]І5gYH0!$O3x نG1(!0$t$yoh?3EQ. 栥s*!ʖPqup.:K SY={>>@ }BH!ML,lh*3Su4!G NI!Q.V`{cYQ?|eư Yy?-f0 Pu쿫>w !=VV=f&_o$x@7Dc]\.|W%:νN}OBHCrGpѐg2< @. {pr .~r@H)BHB$asvs _w$`ZL=(Sp:%!ty@(}=zr}yG|8+@_Ř]" "v||' f$$D(݌VZ<$ttCša'"W̚x?8@`- "Qyodp01;Q@*7sx;@ nx/ė@ӯ ZV`Gh C OVpz zz2X@_H a*pXgv`F`ƺoE|P˱<$^g;B]gTJ;:$>լ Ý粆jt|={ux X !{DytN]xl/^*2ݒü]B^fv!%7hy 玲c㝟sG|`0W [8\]wQ>wt1)vy4`!T#< <|= ,Wtm'HGv*{p*IENDB`PNG  IHDRxx9d6lIDATx^A@aSӛwFBa⢲ijGZp$]o+'Á ix,fxf>j h;S9E{T뻴 *(@;11 k.oz˰Dh9rFZ`h4Úv{>0MwpCsd=M 8 f#L;&'2 rw1UeWs' uFEZ5&:Ʉ2 -#N HQǪTG55|D0U > *piI}9w{;9瞏^{#$ޅ9v؀kF80k׮;w={SE}ܹs_ƞ , Y0K]|y… ;0~/P/Ǐ߼ygW'd` QXT9 ϟ?ބ r5Mp Z*** %ob}cK2ԩSY0P>4zi yvyW$`襥Ys}SL٦rȕ_BDAG:`:וxIII(m綵pj@6R;wuKK(VL3eO’7Kd .Y}S0&?9sf2<2Xe4 gMD1ŌIˊ[ZZGwΝf LZL66 X=Ռ޽; Ei&׷M\EDRM;j&j˚,z? 夙^{D1ύk{=\I.9\u JpA{칀祙E L((@ 2&+&*V$ߎ1S>z߼y`!kjjQ]8 4`T|%Ifɳ7<#ɥLhB iBt4Uz!Ij!R0;w(vؾ.N`lX]Qt~UעL|J`%6Y>|p NokѨp=J׮]b̘1; L5iRTTTׯ_q2Ѳ6v{4YC8Qze[F' ^TV&R%v֭yLî uuuqx}K ;v| p` Xٳ}߾}ىt@l6X]ms]c7?E?LIڟe` ;'IHH76Q h0G]NwuXOhܞ00 EX"m 0V /3J:۴-񽤤dr||ܴ+&ـ,D*-7eY1}Bk1/ըpl*((I4_ACw6셶YyиP֭[,+랄94ZjK,>]2Yɠ,"`o{w0 a N@($yv!y0ѫV{ˣ_7۫H#7B-w{оz>F^A d2B}oW+;8vNHv99N9?/9V)v leG=| ս {>o :Z}gxO).;kRibV'B_zxFq;PqPvMq灍l^@;oA^nqj~#h @rȮo'S,ĭJ܁;;>BE`0 X7 ؀;\ `֩ < l-\㻧PlXfX#w#wh6>j{"pG6>XOA7vxڰ |} QJB J`c 40tD ~NL?̷X IENDB`PNG  IHDRxx9d6lIDATx^N1 Ef<3OYzqyn YNO7u YUw:׳?L@8곀`&)d^~L7w^@\v=Ưw^ dp&1legk;qKX_ 8! !}BK[ 3\93-OsIRS 5s찲bRSMVDR9>K˒<fW<8 掹/+ړKT_sdiRL- "=#|;OAف(sIvfv&pz54iy{%^l! V57E`f{5yug_|y5^ф-$Aof$z6ׁ(̙pG KfH#L*_rdL0˴z5z8":j9sR .ٝ3` 4ʓLh{zԕG޹Ua]ﮯu&!  PT-m)A *-ii R@- J QH$%% ؎^;Rqϻ'&{HY̜>P+oW0=o,`dJvW.aZw̰U8LWf d9?2&s̬ `^\wC2h6d 0)Xµ؜lvJ3k X'UaWU]V5\J&">J:wZ,ܰ`B%ӮHEQy_1_4U DfV# ;~Ȯmڃ#][V)!j[%`\+sfb `wB%bb ټkгjPh2`/-/;+;]׆}lD CpL~;x i3pYn~P{s2ඥ۟[e0p7'@I,b2U ׂc< _؈gcKЀ6ٷKju2f<2k2Z2'S"z ,=53{qQY2v^[;5O;_ 6lþx=Qi\ hZ#\3k?5 \ p3(dr5\ ARKJxug0vCضy/z Hk;Yd1X5jL(njr/\yLYY~!3 Y"&[?$0\RHHO_%A>яjlxЛ:rY ֘}|,456ݫK1p:G zuMx`ڋtjyoMd *jpY^kN:,/Vˮ(|ɜ0?dRE ,b^kpȗh2pCy{[/ Cٵ(8e̮Y]o.g.D(;>>^pQfkWMO,;[^ל^cNjx°***t;^aFU,dl1`Π=˜zܕhV/H$Boxoyoք,ܴ,J#*D[5HR|v=ޓFΨbjzWos uVX^llW8T\YYI%? N%'YUg:+1GPzsB `R[Gc,f[8l/ | sj߿c<[&=sFM*Ng1޸*nZg+͋OVs*AuM8GFF b1ʨO 8Xn|]-l˚{NN|Pz3g8(2B=P2澳KG"b?< n<s(h,Fֵ* ܴ޾q8m*|]&81yՃ;26&jbL;ZAHgc2:B6͍0Z27EM-{Ҥ%3h+}=Vo@ vaxx0Z"Dk-xbD3AkF 'z3 e!LѺ?*X.`p $!u{w񁣂!<00J8>oJ?+.E]!N'Y"ɪ$q 6 !mS=-IV2)a kYHzb0a:*?Kcy `Q&p K^Y4$p΅܊M+-ӷUi|YX8{044h'ToUa-*,E;Fc8lv\ȉ_O(j{n qaoo/%Zaa:!l[d=OqaX峑lWMG"#cȧnF`$<ן숛1dxx#þֿ=Q8dptfEEViLͭC`CsaS666UBc`Ȟ})K$lVf*wY0RT\]]*x5݌,΀#r͖M͵B-nnٚhRn ClmV j^ -s/a>%2r>YCȟs[f2\wnJ*dԔp9+.nBiexH-eA 1Qݥb_| mOmG`sUؔ6#յPYPp3(3wj)bj60A 9I  yX8* +?zQ{rb40AZgӏ]y =z5oE>a!3hT0Ҍ之QL$4!11{M'pm]2߉~R`AQS2RJ5l0wp'CZp:1ʬB<6SHDž[YQ=-RODe-F[0yP-7xYwQö?Bvΐ E t\AG3 } R#޵j[t*.LXϳ fWא9. X ,G*Y@YBE3dN0Ul=i~P1!mUOBJyʳց z|beEK6^H`IR%C5͎LlvG0`O1v^#~5;0qF|\Avl1 c@h+^Ŝ;Zaw7`vIL,#,Qz{qTHeÀwdsI4U;$a:.ӊUͳ# ;M=,bY1 jH-p8!.娞\y_HP{ X>b5C]*ܑ+6v:LZq p#x7|y ! ;ؑY1Β FG 9:pGv`N9 ;A̩.w{;0PUp3pGo<_v]GPv`宥a1 ?[ X@% w/Of ܑ:v#C{rn@懀;ۻ]w:~x1GSwIkws}ׁf/ \Dҡ;nK/1[i UIENDB`PNG  IHDRxx9d6IDATx^j@ Ѭ~s?G= @bZZk ^ߚޤ*W֫{9tW xLrd^Q]^hkþ:z71~] x%@ǰ9v ; @pk3C1C:4Ay- 3\^sfY簓 `4;kk1:yjOk,)Cς8Q7vA9qQsiM9 nVXKQ KOs\O ʵ':c;pkJH)O5qXJs.a,`Aفs\@ `~` a 9"YTvdKpòB<'J5"(ϑL^s.NtWCWR%0[j[`v,#5sػG7~;{v,{%MBkAgg3q_:Yjœ 3y`O.1dd]12=Yˤrk+G9Lx=Fge~~;?9g:^fZhił7@.!TF1/^HT 1B[ B0P t̹9{;dЙ7yΙ^:] j7:Tl D Zߘ9DN h,HI=dXF`w &=t)8r%`_%L)7p2T<,/ XL9d ZxZ&eʍZ3Ԕ-(YA6p(f-øL 5̋*PA f+X9WhM.`,\];sr 0 :n n'7`!U@ee'>kXV-7XCMk`wA/EVc栓@dQc>:ᢪվkE2%&U.ͷ'=#)|n'r_]>6=8 [6aD5s5rrÙzdYL|bUc#S1cf" Gٞz 0*>˿Iey|l>,0Zʕn\>w!z?˧T$Ic1 2qum:PZ݁{vճxS*U2MUh&pr)\\X;XⰯzET/̤|bpj@mӶ ʡ2*4vXVIx&EUxJ%hvٟ%+0 9- )E)z@ܲZqhR, (U~{lo>'/At`pK  =vcYd YsC.cYޕ=j(6@uXnkfeX1CرkD@ 8ǤeL4B p|NőɫgOp$X %q&W?K(((2%d؇TTcхeAeps@:I9dW‹ E% Z` դ2l$\dss",,2M%0%0Cw@U.-!TLC+{i\A+rJ*w!ka=9d =1" XWn_o<],Ҥp=Z7`,k>kN-3`k\fwU)ȝvfyR Oe+W'w؊>\ j:r1" pzihVpÊSze z딷eti Ŗldx3S/n$A3?,gRh sC[++LQ1 F5myeh4/ Q*; ж$3d{v2`wbyմɀ̿|Y7dOS)WqZj1BtjIAE ,sN9klsV[p0{?1agfiHq%T,*̅2⾷7`de38m_nHۗ<Glʿ7}\P˃kxT.6K(8ŎVSY{³V-\ p݁ Q-LU4vh` 8.Ś9HC\`VCV%G/jm rr4|j~%Gر8HW=͕`rTNd*xUXk' n#]ymB*ŽUk86'_Ӻm$ U=&- {@S1D)RˊAϿ UqT"6)V Ocwee1 \\IgF`&p*`G`~p1c _x76ipE,LVR5k8Pulo~kVcުr gq7P푶p7]pkrCJÆ e%9;GpmcƝ4ÿa*h[~\^U'@P(zގ]`exٷc*"$E??!7 t|dx~dXwCUh) vpTHgnI|G+#i{X#U6+, eǠBca )RWYZM\<|+n^ ٤}wY?<3P!~6kڊlR zⓠpA>T7_•^_p`~/_]/*6/G.n[Voys*'Ng'yΞs~qњ)<ö  ig7W1^.>e C%)b\ǨPny /:;]w^bcOσCDcCsKgdKhul]Y*uL ǀ`aN3X>fKUԼmu^Ed!].ʓ})brcd /~S*|Lr1jY (NLB Z{C,cdXVN6?FB#'L ˜|F~zHC_Ycxt!OK[YYVcx}Jpi|()+jrVFɶ}~:QrHAu_>y`;Ҋ8 B0ɹaUYy+ޘo(rqyX꿻8P[;5ۭo?W Bjܩ2*E 78(Mc$`r7y[~ c5+6.;4nXW{XS@ 9 C(驳<' ؆W\}6uBxc>āt}8=ҏ?7v(Ikd!W#CאjH3Ͳ<[sQ}W`nr 8)bԴw6(Q~Ns\&sE"@J2/8\-}V'/՟ǐh |:AKLQߡ2$ ,<:98}ÁCNJxr5+y"<0>?AzM!߲~7C!3(94d o*_7r=-F/`n P=/ P[W5@nPY@CBj|C8Cl8{c5U(S #w ieR5G|}R[ WIwF,e;ԊebϺcF[8l, kN͠5a0`_`ZZ `Osb Y^&Yg`5& P0z^&sCs`0}= dLKB, .8JkߤpxlаM &&3;:d?aJA ;{yz2 ZFkxw Ez ;F.vQܺ8 v[M۹{q l\Nl|\ ;2 .ȀV*N8(y;l8OgzA ]E}t{ xO"h߹ \-Q؆2ҐM Ci'IENDB`PNG  IHDRxxmY IDATx^N1Q<þ +\RT9r dǩuik=} 7=p;[>o_O=(L33Ԣ.By5lp͡*h`bR=\z=t 61GAvUlȼQ6 .cwm pc)VT"blp΍'{D; .b] [씱)Jpw&蔉%UM'| .6&9׸‚ځt lsLpSvq/6y$з;[hlspYݽVQ}q)ENOoV(D[%Vk+r) fH'F|Dc5H"F)i{ڞz2`|Zsf:Ѱ+3]_릾1gմmTghl@F\d KBc  khjoY ˹ȖfumUoW0؄Fh``˳=XfN͓Kt44Z۟Lu9MFZ+&г ֖KjU7UV p^x" `aeQC@>س/fSl V)u }S0"%G'Qz, k@`}ey=N ΝNO.Z\xx _)_pzi”$p3|zᲔ"plNՕש9)_\[(~Z4KrwX.liY,-Iv:Vk8x-\*Vbr1#G]zSKm }mSsoTxcNjsTqr  Q{KՏGَK`-N enCxLÞNʌ`-7~h|kB.HϹ+?OX[|*:v=Rl1M]U0r/;t8גT-NA{~MnjZd<9MIfoJnQyȗmhye.ZnmѸ=vF> &XM3.жnwTCkkR?dy=c*zN='qˀ|?[x!ٵbu^4&]'~S,gXeDwW)cm>jU>b?4c\\Z .nՂ?`U^5m4rΕGo>jvjFpt{ץMfb? ztd ]xjepz0c=+,H/l56yTFP_RK[_ dl^uC0ͿZg Se0,5zX#Z$^˧p4ؠ{zx9khMpI*αVR)Jyn Ǿyi2999kfw|cvg`νgT0Aj BmP[P[-j BmP[P[-j BmP[P[-v W2[<X4f>d)G8h]xx- i7l`GIqu~=6鬢~a+#CP O1>چsTZv':'=XH=`h[L=`h1&v W@ާY+6HLTIYڦI+ߦp_6:ivE38.Ζ<ɛ4fJީM%]DF,dLdjZ,ٵyL4y>qv~h~00QhL!9WZ?=tv;+|^i9XX.a"ym[((X^u8<˹HɁ("ym'JUv\"Lj6L"/SD!UrNqum!(djzMqUKET Ƙ!)OZk޹L$&߯~ D ܇'5L1Jɣ>]8 ӥ/&-"^kmRsK=4 )).6ans=XkmAP@G@acgXG9o\.*~؏F+YXA]dYA/8&s+#.aQ9x<Ve#("0=WkmoHmhh8专xQms1'#0dHZfFI`mX2TFUr":)( $jm;:??UʟvZkۣj+ Yj $E4qEly6NVߴidQj lYr1r۶F)ͥD/{\C7m92H\hp!Q)e䐴;K+er#=iS'Cc4$mk[&`>FE>Act$kl!0HW_Hm^NW"]Hm[@Wmt/F)۶wHU,>j3,mxv?gV->TyAc / Ȑ8i H#Y-CS*Ԗ⤂MCEgmMʓPqHCۓ /"U(2Ttw&^uX+]听m[ WgHmmAndlm⤂-m(LzHy91Hz\Q[.$y6Ee:e8L?MndIDۆH4,nRhXe۶'SJ?PT~FѶշI:yY?HZ3WscՃڧKm)$w6$494̤R ;E"MݹJ)q%|$)BZt mi\ڈ'r*3.9Lf ض֜9£Ej#^J"He2eBv+[Y$nUAx=R1D1YjxW~ 1b$PSpFڜvi#iXj"VJC6Ah) (Wߥj딓Ж@~WqtPӏLjK1mDՁt60EY+s AH7 :MdWmҌ $жA &0/t 0ýM]5qv,))5iH^kۑDi+KSSmb(w 0hHʃhKV. ${ܙa637 dCb)VЖP4w$7Bi 1HI3AXa[%Qh{יk iw96"Ĉ(%N7-B/Wn, N( `rw`wM6d m4RtyiABu-}68ȐWd'JRΞHQm^Rb}OdjgHpwBjL>i&ǾG/4]i`x8_d9n6yLfmO|,d)ϰߋݰvVU?l>` ڏYJk`z_c7`\Gcm|c`6K;?K2 ~* .XڇЈ~|9mvD"|Kj`s5jnf}Ct c1X̵>lp3uvsσOzk#kF<` k=y 64`˹TN^pW>p a64v]Y =^qu ǬInm=X{ ] p-UxRu p;% w1pԪW;cؓ [B@HbQ8p6AccX r[ X.} NcwcЍ)sd€8'ϗ `"=@ ֔S7k( e0A~x6,~>9-XMtmsUl ϟQp=[8.0wc:u@G.#c1͕>`UtlHdv|6NqZ;8> Pw6~;S3%{ #p/քrsr98c4urKh{6 m-΁_ڏУjfYn C'w +?!!K]Z7bUA6\>y>+W nR'FkB݅!Kt]Ss+ƮƎ]#c(tBynk7躤w;@#;ח`T5a*;p1o @ XU]ccwQ dHksW4Z#Tj8i.@˹0Gwi6!>Wi}WS8Hk[x;8+ ?3;'C;&`) RJT RDC/H*DAjEAm)jԂ4)KH7BnTvofO^֯t4%Os|>x sH IIOUKĉU.x ;sm*cÖ/u\@;G1[SSSQ<%tO>rt3)ȒSf\GE.>Mn =@!Py*ο 4YSGuk U$R$.1ԓ#{C~馿$d&h94?[NCޚ@! v[_7i1eT2 i GSt3qNm:Ɖ<>τ BEKA"t.?\Jڹ44e,uNVy<ũ?C;ǀг!fF8ppN4^,o)IYVK-CxiN޽6?1(,|C@ Ηc僣7K^} $6uIT$[sr<2@7 ـQ!؜/[NnXq .G&UN)lMh2p2tx$}gBl.lZKۏV b4Ybz6 ytw,g+vՋw,~˙Mh[I\aV5Š!\9g*.Uu5Ta'B,_Ώ&m-1&5;յ\*@s;k ЄW[9ViQMje=3ycmk-Zv 󋋹SxFVj[9{ۥ,m}}W\c4QQ,aD{̝WEMrf~[п1ft?e3AE \8rgGrE wgrxv\1Jnk<%d_Ԋԑ@q jٶv:DP̂uͰNWܠ; OWw@Ҋut'̫gfR:YOf?yb,?hdᾏ4f s!TR U*PsΦPiT!X,~09캱 l,o3 ஶPW+ T*#or8Uzc!v/F}";(,^+%׀(oiÀhIjHRW3\Be䟱*I1n_a# `AM>/3>J)sU&iM% ЗN=svZ$r6˲ak|ٳ-3xegp/腔T gp f Gy:}N_݅ yQ &9['L7[)Z5®C,=_ZG-H(9P]N M02g᥃𕷡}LaP( f$q/:Iet2 \|G_'OGSb@fvMWA;p|pPݲЫ kAgݟqh\%Tp*p5S++Hp4Ζ-mMiU¢ ي^ 0pVUqEB&VW6! Gbވ\lPG@˄ꗻ,j- *ɰ"ӔydahӬB4`fL.3,m>|k\;wbrh%A=0g ^ܭw@psg<3SAZAf:0ΰS;y \\Vxfw=9s+je>&X}yQ=&-J)bч3v|#*?l%I1x|} t/q-@|ƽ˕8=r Hqjzcu/qwylG=e@89R K^yQmTH 2Xڼ^9)eɹ-D7O]儁a-$(GWXa_uGaCۂjz#g,emYJ -jZgR!hQ:_~] ܮZR^0亮OirKRŸ!}lU?[v]FFDn NC,`@!%DDH !" ǀ0q2ɖu6+k׷>9ka5M9֜~s~@ * ٔz!m_D$TBNCuR< tZO[^}rܾ i-H6 sL"UjJuЯ&JjhaSQ'Q(')Ez^`pb(Q96(@&5-~BG/CmXLߤ7~妕+L@` :JFkgZ&b5'O10]daEySyZ@V' $'p322>36pʟLZmBlYm<̖o%tR{G޽*'J@;յ+z+.\JUJ#zjٕ$BhbO\tiQc@$WQ.R ,-02F-mq:d^ŕ\ھ|?lͤG\8)>3n 1VV|8K_:*bºý|DمM)СݒLS&zKI]jGLպ&l$Aq`{"XؙhE)#y* ~N(Q-]q^ԍ4^͜y䕤3P%h]̎AsSw#%aX\)vBo*o,6jgHmho\\VMQ ssHԝ ×eȷv9[*uFl4sK*ͽ}r }m1^[T]w1\kP6\s X .`.Rq[X"*Q˨)Sl.xJZt8[L sEo.\KjB*K!ZW%L$s_;2MRYv"] 9W2'*_ڀHzw/CȔ٩~ulv@ͅr۲ljoN Uk-mM@;My7\{~s[;C{nŷF >}&{ver1l܁oL?|X7Qx޼UfLU8d!SjՄ$@b a*mg.I!4^l1TUa1q DW XVAkP3sâELߜ|0Y< [-—Lo4+(r>RHl˔?V@5lOA<qs|^ 1bq,>0?IշKS\ur/WW'aeNݨv.& d.h༁DϽrrvҺ 5tЊ:Ӳ5۸K=== 9Mz}oltƛ$ʝIZZZ{}trٴՏxu=uZ:S#$o&YҒ%DRƻ$ˇ zYuwwO| b/trƂEFhIeb`2u%$?v%mŋ&)%HH'Qodi.EϗRK \b[wik볼dS 2Z0i4\F8߬Ro:5Sw;\%X oXX$}K4(ZA?k3… Y`wq擿1.N7jQ0-pB2~q%! YkV%R"LDǕkaS~8LxQ`}G8EXYq^`|' x>ŏqo6(iBҘ4mR'2'?Ӎb:hlTrxB`K{ٞptOv6clA'Ĥyr7 YJ헾#=1~c'5uKZcl|`i.ax7 -?27 Y%V-Lq? -zc ڐT-w.֟1pe&L( s6K--n8,z@$}\nx=j;Y\DT;lxjkӌ}ܨ<vm{.p #Kt.ZŽSu4, ]V\16\ pظ֜s /q3?n^S 5?2MhVө& 4LL-RzZ :mDE2@E]J3.l]) Q"888 [oa4r4 i7^V}@jf&z`"hMh .p6%D<2M<:Kebh\F8xfWkJ䚨@&8ALh2ccp^,.|;PhPNaigOs74-Upux!pG/CC' s`^\ d F-nu|CXk (!`5xnMc<;?-&Diyau'bDp5G4+<2na5g-yxc`w`Dbih_Y\`\F}eʗbqOY%C4w6\kj> dݱy|FqUM@(pSί&|l8 Sѵ| /gV~IENDB`PNG  IHDRkۯ!IDATx^ъ0Q{vZy쇂-(#0IQKcB۲:9/<$Q W/` o1z]%u=iԋ{b6UaA]A^2 [):8@_y)96&=J0'SmWp)kzYa3pBc949&\ADȸ1[C ӳN;sx9F, Ùפsp/mTNJ1q9&/\q%6 ٗ^ELevNtM 0.Z>g- X` O{&o+;L=oa8) k) }~1ph bOxRXئ2 .`" #%h}l~J\<ӔspSb{I[AVjC¡n'C;jct.tz?^@8@Sd{!Yy CgGHՑ}g.TbDk#fOlttr9-0yM,#c9:7oC.~OVLؾfD/æҡ^|.IwXD&>\װ$lUl_3w6cbl00@`&,M#I(%K%TDUҠD*%MH4ڀЀ xxlzwXGќ<_zt̽lgyLp)5&Mud#M樑9)KjvtSdcDcd&AANLh$N4l~H㣄8BtS(A l665aZ"T YFvsSmqDsS,"u)w ;KYYi8gKҴ!̰^D&_?YK'1}F-H}t=ݏn]> Yڰ*aAWf ^)gog`zfF#ۼEe mrfѴQU(C,;y\ܚڅ@ҁDB^@{Sׁ25B~Fu[`0E\NgӰegNӤ̀Kh[^bK: $$ H&8|(G_#wXވr p8 >͆+kBw|sRIC. P&BŅn^b Z\<9\4p ,sT<ۙԹ,qV}j e(TE)>*ϱM@9ʄ*'T В:/s4,jg؄Fk“C$17GjL3¿p_+x׬btj(II!U.4lrCɠ, ofsR_%X 3e*>T

sFiyZop,\zJPtei\LjTVs<s˥@ wG7aVjjΰaՉɽ4p~!0tB2%)dWg~]inIGA\`?؋'y=쵀a0+U{5} >6f߀""(&KK&d) ' V wa_  @sZm|fL &6¡^;b[x\g n Ezó 4lIE.E-&4 !+}@I tq+/8~VN0@ 0> M͈j*\(!EZ*T0 NN՘- B+ 'oڕfݳI`+zW Wl`"97::m/lp;֏W=ҏS8l6)W^cnkFOMp)OE "a5`qp\$z`}:jf–"6eODhlKa€Gbzo<Zs#ŮY?sKÊOˮs}_;k܃&~[l1e%9ffs9`cY D+mO3v:7*$σ0zx(Hթ\(hT`;C*Vdixf?CrT_^ WO'TтF˨"! PT񯼟'5l4hTf&jgS~טDMy;|vۉmw_fl7Av{Ac<K-y-7xhU_,؅2 6y |MuʜԬ⠿/b3 +V=|/ǁ/,T&}j!n01FįD%rY4gϾ8 0!o{%UX sJy:: $&nB(զa>;{_(& ayk{?ò-ߋe8گƒP|}E&|{B<ퟌ[: `-@ g~za7jа/Mm [*܈0+(sHUd-oSiNȰ'n[Ό?0DAcˁp6 v)|1˳GV2l塗ddѺ& k? yIj9L4*D2^ЁEoQ2~-<'ԠIj&ҘW KT?j 6wC(j3j$4ڗ^I2rT[G"lg&@3Z:8^yg{bwԅV('PaH x:3@NV߮M #$x6w(Z[|m9lW քB~kk6\3CJ@h.XBs0Ql MmV{aiATx lW]d7:GYKFb6q0QTʀÔHc-_c&l@3yhe"/'W*jn/[kΘh'Z+0g5RF]53-*z#RP q /]Ng*5–Vs*c[&M@_ ؤHa;aQva`+3ve۟bMR BH9:|yK [ ljn5l_]WL5` #rN8&ԵM(؊-m7rxm<_l(P Z957[ܐQW KDVaZQFkɞҏirCt;4j=l}Xʞ+2B| g 1}W䛋oce5^A-aԞO#/~ /`ԕr1)ʳ aTV =[B9i%@[tGRnUtyTe}|w%7xn^,`kJkѰajKUnOy i ЏOJ'_ZpܓeVϯaN?¹*J;XCiLEаվRQ~6a„U 4D1Ѱ i/R)>pp P02%ΆX*Ѡ$ܴw;el־|UUgw;u'hJA]2F q$!JMS3̭vmZȱz?|w⭋S3鸰}j.:i&l0$8o79|K`FMAޚg/rq0gV*6tե#.ǁ'K\$W&ז,u:+Y[১l#CˏwºJ{SWz I,́'sWh5l8Մq)h0>/N5d5!btg˞+b1ZzYС>e0 o}25'B2l:/g$ön~OPwN٫QbbP#.2|_ݸnn["J6/۞gcZb&Ϟۤ,L3Ws PygX#Bb{ɡt>s/ $ [uF֎PU!R|1[c ж^!tV>')WTM!r>;fuÈԤ1`un *B=7<=7NkM=b<#^C[4p5lCN5 z{R\ U! h}‘27i~l4aIJ( Ќ0ՠم 6OMLzK) iJ\sLUxV)%s^<݊a]+xLCVsmFpm V-LT"#TTB+c.G⊜cn$ܠ`6Up^ctҩN9O4lhТ3 {{& :UV[:XH*5VZM\pC>T$ vҨLw7 /pijMjjT#fT%"{85ŋX'Q.PT^S`L: QKB& u*^yu oIaSlF2 `s8cփrIkc|pC|5P0 0"4$}E!$uJ$HV>&,ѿ|-/H%?Y.l؂ 6pZ{iN_w43b}6ߤ0&@ϣF \2F&$H &Ez\*zzwGldCߛɡS͂MNNYN.>diJ 4]H~N= Y2 >@˔)째@^馛t+Y hCC-Ck͐J:8$DJԥH$bC.!wq߫nQƥB.e*R<:ajpx^\oߣٰ%Md*{9 ;Z(I:PEfΪO]ُ5Qg`KxC6joZ5%JtY}B4Hh.뫋.)4t! l5eYqeZ&ͱe ݢtk;f I6&m@:b 8ȎƩ`)H7v16nfMiqAd`FO9c_Ldmh t|cMlqYMcht<pUX<6;Y([ͰMI`3)&,=9B(\D@htf Y.WD ={+]ttA*9n /@4j>qZVpvp,Z/s$8S,@>'X!<ׄ`d4Ɩx%C+MKD\|9/xZV-`85^,N2N51(T xˍK<7҅EОH5>tkC @+fUY&^}yt U`#pl9d΁Kx"C$chMÔ+@7,c4p2ׁfЉ#Rƣf4AoV,+@s(W=T p=zf%Ş)g6wxS$V{cs X`o2.<}rqc߁uY77IENDB`PNG  IHDR\l+PLTEyyyyyDxtRNSbiofU}z^]pm{~3R)ka86n`Y1Z> g-x|W*;uDy $'M$P$ zˉ3FafZi(Rn柫wl (WASLAviyTNuO<"0Td`&/uy4ޡNl<4T7TʶQf"9TVzT:GDNu+Ptb*˭nf\Q X5Ui:]A.%pKӱP.=<67AO)ͣ9VemGr\:FЩCq+zTo.[[D]7^-t`8q7tT Ni->ODNa QaIc1ͭٓ` *{ A=\&9p 81Vfn^O*.5Q] X h*& ~`UჁ@8+Sͣ,/m85_ܦ ӱLL".sOp 9v&Ob!m3ݳ\.mUʻ PǪ^unprY׾.1㓪\SX}E3L7aEyѥuLO9dܧ-ZDp_tʺygD!608VCja1g݊(F|9' [q@t[.oBKyo7Erwww my[ 5gAf=" AR>ݎ:769e \Qn7q':٢qtyM~ KOʁ] ]d.vH&O.:b?mܭ 01|'FۗسkobN.Z1ȶRU2lNƉLe:69082~?/}+yxD3؝$ƢtPBL8G^s䊕rJ?"oD%DK7L;4Bglnp(KpQQ]쭣j!:GáP(QCGNB#d{\`E>ZBwC?'ա)+{1sz]@ Tv: UAPȿn̤sAj}f>^{ b LfHH1CWJ0m(IMj) (DŽ(`ҕiC P2WeH%T!=J@=aBRE `> z°?fPp0G9:|%?}\2S$aJu}UU 7tEQt;:XU1*%T$<|%T'DݺI%(?v ՅLH<]g^d'px7I[UjȼԠ*>!'O5JSb0DK`bhk!)奔[Zڹn{nYYYyf>wsV3w Yހ2gzݶnep;^:7 3ap(< :=: S %Ƨ"|^W7;2Ö?5<fc2+B~^>^ʓm]3N'ݞ zKC53?Gx4 p$f^_! @jI>Q, ɏ('1{ ^*;i0+DӊP8ষeϓΣK0(63<~t2G0`GA$ D2r("%Re7#`LEΓ)!V  ͵`Q` j~A8O(8+ʁ XF:&rXX;6__A@dk0X `ml=Q  d<pX% ^)vߐC ~|qB6c $# JͨM@*d1pPh/UIQB7 j`m).*[$ʚ+D<4! *г %L  Gi~Tc C2d9}t%j#ܭNAJv$N2AnByo2r`'0A໎/ z$Nc:?`(9d=Cx6! {,#E%/QL!(8zͯRUQ| n0dS` ^a p59 L +:uwp͉$Ǣz{\^Dc{_P'+@%C~qp{>!Nahq܀>F%  I$Mce MGTBK)ˮH>+*a &kJ业bYBc!x ,p uӘ ﹁v2WQ aϴ* \ b"ɗqU]%_5蓏‘cQZ9 [xc{'`@R;H0q[>_PZuQ>VSpǃ?zYɱvGƈ+;d>'~b\83Z[6ɵKVx@ʪ.mx,Wqk0mc1xsk)"xg-ٴ]OElƽT8P,Z[y8V U @+p fmT p14J.`uf+p*B4(x/#aZ?# v40D=xy%ւD D خ!5vY່iUeԪ￐fX $Qhxֆ݉aמ?M٪K1B0#YJ/d BroE9R 50J9$GVNsG|?)yGl߼d\0&PAEGژ"D >wKOèZVJ[$%pfK$Z 3e9i#T!- U?O `Z "QM~rif\y翪@T#\l߳`(0fKm ODeװ0{YL5&"Vzsp[d=В/{׿~c`H`c M6_^~$6a!苋(D$pڸo-J{p]=zΕ_"rjB) oO0s(/ )G  zk~5?܈7@}ʠHf!Ep/6'^o=Z+fm飯k2h@Q%c!0t+7nm?bE=B Z h\5ZV'6= bM Hn4Ҡa칭oʻ6 v {@P^ 6Kx%m,0ĚiÌ豆kJmF4T p]ޫ1WAz"$]#,_<[?{)lGbCPQSW;VVyC)]`ܧ 9ޠSR1|朣Рp @J?B SG@hGW283=8`hAhb(H$@=!SŲfN$|1B +Ot@CQØL!@ToݾPItjkj] `V62J=FkOca?K)aٲeڟ6b 0TK@5v x/?p[k݄g)*s #ot-c~|;1ckmN^<`w"GNA E]S/cy?_ˠړ/ayǽl (P tC0 JD؂ t5V^|*N:d9#2:UgČ%f Lj@Ye Y{ mj~=j/[߾;=MBKCLeI>q1`ó{$I;Gn=?iĚ)Q2潂Vo^$ۄ 0Mʍ346 iʨmh.@DJBԎt TOa5^ IP yi d⩂@!xS.gy=nubTAq4 , Cl 8 k8}@TA 6Jh\eRse`cI[щs0j`o{dzE#"\@̂~7dh9@QwvsȎ !ѐW J:0K pAGrxIAS! ! D14:J`,*U,{4; Aš DDJJj,}c" ]w @ٌRB$Wc؊4C鳏ྌGX 1XS?Sgy@bهki5IQ @8J˂GJ_\Ԙ(I3ƑCW X#e?] 7b4 ԭ`$QUA*`dks0 U2 qPkXTi 00(`sRʕ' >YÐD @(r N!ACC jQ|]CpBJa$Kɸh0d @:knȆ50='h0õ<Pt [@1%Ma" \=&t0ֹ钂fϟ[Ql=Vr(O:`ah9P>`Ys rA@*Xf<Æ9. +d7Fk0bay n)@dfiXp͍ DIENDB`PNG  IHDR qIDATx^{eWƿ}ՕNY$mJb$I$?:83A|E%(F C8J|a!mLO:JWսg[n|lZO]RΆϹ:Y9nk~ƕز4\>alI'49TȁS9BK &qL\ 'I|ߦ< 8bJa0͛ƐtCǸ\d=2e:<8P˟vv5Ò9wuY @|w+Cw{9+wPb-"Pf~Uy@RQ%YѾ#;N8σ =xg::< Equp,!DzqPQ.O@(@iXkO`X(T DL&7.قmh8k(1R#ǁvnŶ_S~[B.%=LK!@;E)pBnbD u$HjX ]L(3Q\nneA"FzȮ￈fQD8R7IC !umO(qVZNq g/;B0  _Û>S1r|mPg= & 9W06 QcyV|/CkZebCI'U"bV. Z`y& #PGh?o_i7Μ#IX>#xh̑b(ɥQ#̜[`( @Q8b$Ew%.z, rP-kA jkRc%[#)'xf1X!}ާwbpݎ Ҏ!`(o^͍]7LGȄM%]?"`rb;[}<%"A_:s[߳ eY NM@w@i!6Hrifnq7qj@6ʅēTx W=|߮4&#!7dґ A4Ok.9fr78Pbr!Γn|åfVfTi(rr2o)9U"|T538Maf6 ;({2AЀ1,^r&|W `MbTI9Qzɩ>bjiK8gFޫ0Uu(߰gg};]O=^AaDY |Wsf麉Vl W(~#Dt\/:ۮ87v%A(ccl%D"ѓ|a$pHŵSCu Gt q ]yi-`@`Ű^@зcX]i ~ ⳷4h}G]i_wDˇ+ ?\wvlLtL'9xCe=}RlsrYjĪ:P:q|ᮽ*,y8V Q)J\gp)|[ܩQ?k׏nrFPesă):5 @9BC)*6DVu Q<6 +_eɫv[ 714F2k0NAtWwwH|UV 7~hU%Ο. j|_>K/:;{؀.  X K,@%_i#7t)dGJRuV>y@$W g% W9ḣ’GOm߹/h/Ñ3KD,& 0q=tZ %'aõ| z_hiD,jo:U1݋G $vs$ZZ`8#o8}͢l I?܇=# ]a$3؈壆@05= - @ Z@@#Y&4?t0@ϫ04# ŧ _>,Pÿȫ e8C)D9:"`8I=ɹ|{={ra]`'4pǞjGhB@h8n BpͻbcWGX08zJU;0L#;9K/Z m~[# X.W-fkMRܙi<7V#vQQ)/h T:i#,M'``7HkvRab~ `( )EU1=B$#lG'0r:ABN~ `GO(uA A& vnϠ1f nL:wbsDvtԎ`"_7C> A`, 1B={]-/xփ?@I1Aj؄ᖟ''0nXqRnf Gx?ĞGp?GX4@pèoCW E <>i<=5WM1#*ױZϏ:}0:"쿅hsn0|gXvSrz^%^"~?a ԽDR`,B0`KG()PizI+ OC9vZNl=-`N`s+{PZ L)  ) BA@ A2Ԛ#@m(6KxB+|  k%p` DrSYÐ WYΥ!tv,d% DQ0ΒPv J@н!3A`. Q, Zd99 w`0gȫ20T(7Hto#IWy+@J?ˠBLL@ x)e#8jv `Q5 G0`J9SwC JCi PNs4@; Kp3V H,+X~:ޠŞ@(P\57 2T!TPAP,υu S!rp0nƒ gA@`z\0G(GA BP  jp$9 H"A0TпAhb/5D\"6Tu4 [(YU"|8昰U}N'u** 8.P #ocNS@}etH׃K =VD/9ͦ?eGvd9G~!? OA0c H.`A\~-w V 1O`I|?Q.3$1|lpϸX_KKcG_IENDB`PNG  IHDRIDATx^?hA|%%Hm iEqqtE?Tl(⦛XpnH* EВj s{/{C$4IIIIIIIIIIIIIIQr.r2deѧ} 6yŷPM(r*B4a$$%(ɮv7]!$d$IIhFV)d% 3Eӄ\ބ"IlKF[ Q<27!&E<9Op7ƣ0nFq4o;m18n:,]ыO8!<&pRؚpsx5zJgyǸ[1G;7_-6! =xL6qW=XkBHM'ѣ& jFFFFF[f xf$y@Yl1 3k;5Sgfǘ~YzMx 뗥IlBQj s A5Fc0u2e}xLqw xdxj}:<a\cۋ: vDI~ٜ47a;QHEJ: +mԈX ! J0 j&8&TB|| !Sd/9bahHHh /Cy>IENDB`PNG  IHDR7OIDATx^A @?}-uN$ibh,F Ȏ:x_%'`} P<>:IENDB`PNG  IHDRr ߔIDATx^QgPw3Weo! /0|,"S~zW0a6ɾL^)3)r\B@Č_6Rz#Q8cnY{}D|zc}5 >t,5& QaLiөoHHQAT]#_T! )j2Ȱim5JW?JuO v۶BB(ETsX4Ě%D_@qoFIENDB`PNG  IHDRr ߔIDATx^Ag\Q{7!RB(a(eha4UV颺(2dJB em>5^s2ͳsN@8@ b<#( )A/0u N 7Q }5_D F|N1%bgT0TP"9piJ& XPXHgHKEY`YPհX8kG22pғmiZzAbF+<0:{Aڱ"E=ʫ8A `'cFs;segiÁ-\5=٠bCL  e[` Pq {+ +c.hJIcGu K۾<~ :{}vg_viT? M|2N}i c9:wqΎnrqg;&wp @`Wbs"<^o2Ϋ^kk."Qm__^3d H8QE씝`*1w60nQ9\r$Bљ}DjZ}PDKJF^ޥ܈Bj -5ry(Ɲ]3}SeDsu{SG/]IENDB`PNG  IHDR;0/IDATx^odg߹g#tBW%شfbM+,6%,Yl:0Rc2٬BbZ}1FMè=s_s}]{A;0D@E ~{1 w!a67N3*25y4ϙnUkT9up\sc _a,4\h}sxt%vAÄbrN 䚍4 [7ś8X^G0p͑]\xrЇv`mg!88icPv\y' % A%k =])9ת7?L!W83 e@λk Wo*"M ,`NCpsqkPKP%ehGe`{n Tiy.WE˵*Y>:(w8>YzFqPljDnR,>AGO a jBsXrYMe nX$\X~le9 +{lE伃yDw ČZzO,cjCgP,zhCuKfĒql<}ǰ Ϣ6Ȱְ؇8[q6]<9P)gYٯw_ƅgoZy_;O}[v܇Xj*:جJ,F+@FR,1VSݽ+'j^ƷQtؽ{ yװd&$345[|{jeIENDB`PNG  IHDR+IDATx^mH^e x@Adoe+_ωBF#& msX30PIsLe>_j^ݒ/Јh 7$ z2De-@֡i<moɆa Jj `f3/[۟ A-HC uŝ3Zc}6XZ|Q%B˅hFeWB-ː]]# ڨi$_ֻxj\.lSOSϸ4 Yx[qHQ};^j 6fjfW5h-I_4gW=)tJ ;waJ3!iB(Z,{v.tB ţg} A$oTx)0]8 6Xb^g⟾HcZ1fgp̠V9nzg6sF-=0][Q;uh 3(i*^ІS30]}XM53Q? mG mF IEub{Lsb1}đ̉o\=VҚ1R4qa>ěh_j= e5÷HAalu{Ljt 0yf UMD^:6c8x*MR/2c$^1Q[[h#PEM?4 㒱 @̑KH#O7:U԰Yh&s,4p/Ј۩g -j? P'!H쓯w9=qv3Le:HK?ӢB!@0IENDB`PNG  IHDRnv#IDATx^UDZC0ku,%X%t 98!3IENDB`PNG  IHDR ?OSIDATxVILQ&z`ћ'=55h<cQDE p0" [Xg/RSв 5 ^Lߛי}=pQJnY Vʕ$]]R${ʹ,.!KjDA`3zJRE^%~ν4>FmQn覞1bx*;.~u&PKۃ{iK`%#cpD5'j;bB AZt"*Lt4RKgj)HoO1ĥ^V7 u2B2;#^Jn5Zr\ @vV*R[ UA V A/\es8$D4RSm#گSU<%tPE&K \3q:U[pvy x!| r"Iϵ<cί0Io #Z&TjB2)pރD((x8ꨵ fL/'qQH,>1 r̯ M,*3d]c= 8-%h&DQ$"j F~v3=R# N(-VRƂ3x#P7dco:G8!FnMt,Jng4ULYuTǣIhy/:2q5YO@H-?6zARB=_|2F~iu6q;L 0oMS<{D!@hpʵd=z&o1 /,Cn؀ KrIENDB`PNG  IHDRoxIDATx^Oh\Ufe&fLҤ5m Yn(N+MэZp­W HF4FM#I5i23Nߛw׳r=w}+O=88$ut6к "_HEBr6f=[*^~bil tSn+e˛,V1p"oyt3d2ڌuε:)4_8:'(&n0 QW2ox{=}Ô4N)1F\t~q o<Pai dVpcuuo)2w||/5̌!q` 9P5AT)HA3!=3D``p?Hg' a0`$0w8"{xϿ)#x|v5S9>q6 !aR'``.~S%_zbg!;1z'ּ'"khS&syW=O}CDaݶFp"JY)[\6K5nF}QNOUV\_8 ґvR4{*ؠpުJdEZՓ.7(DLX1(Ata漏vvTE,h'jQh (:UwyjBb C7u^=]F2^IFߌ ^BA۲VnRWT\&T?! Upr([KW/cޏHZ82Z`6-w#%BԒ&==Fկ+JZ H^X 6 X]! n\83쑏IENDB`PNG  IHDR ?OwIDATx^KQDM]\?e,W"D͹HمJYF.ڜB̈EbӉl-r^|?y9p6 ch Fp\rLB FX)v7FTbDLGҠQW`LlO FfH ΢Si}sCH2NTVR R͏PpAKצV9 ;Ob*o oCOj<5_&&B{4MõrzKȈVm]UEXZ:ala^gfxM- +7:N ; RdtlS&@/zc຺pt6^ bBU?P_O72 שTHH:ΎL%q-p`4Av ]  G{ Bпk}`4\ fjЁ̆%ɊR h >ϐ( d0_Mf3eO,Z=Mʽ/`G~0xY:Jwo, PTf{, EF;Yo|$Bv? ?^m̶r]10,uH ,e䄿gW<Dܳ;+) ֟%1&m9#lrzz0_/_p "Br`>'{yfR(T66j5 DqX6ߛohOFŁ0['-/1ARiB\ -3lKY kc>aL}&NbpduT:*L<`i`iłaU jCw`TemEjWS Y c t+~QǼÀefp@_VLS.A 824HnB;  |6C.PyWW+k~sdhK\?$rw!;0Ad}:Z)TWwwGv%\L\JD@&'Mdsc.޾IENDB`PNG  IHDR O}>PLTEWkhf lXa=PYi,tDAdqƬWwVX1wMBݧ6!m-ubYĦdzla{(qZ5kiO _eS\^̧]Z'q6{ẃR3yf.u?7{ ^sb/OwtRNSIJHh%IDATx^ҵv@፝{W,33;/tJ霿L3#XdSFӵ> cTp7CrXm?ri#Lrp`Tf*Q,;yx$ff9,*5#- ovRb8L O`Igp\0åsy>ڒ;? q=LPO݁%1#IENDB`PNG  IHDR/*#IDATx^՘oLeǟk+--(&s"26"P(!SJNX4#f87ǀec1^,,fa pmw=>˕r׾5owܝbp\7#va$.~&F>< Wݷق-vǃy_{NXEvF%͹ɺ1׎,B Po VUSPYvfі@n{+s{/-YH0Ф"C)ƆdCix[51TSaw}sQf A(kcW1Hxo!AK\| ?'pQe)WTTrSǿDQ sgE묙*]c+fgtY]vnxoˀ7`ҦUKcty" k6?ԟ`fS:w7~Ƽ<:'7o| AcE+rPwA_ZG{y˲+G.=Ǐρ ,꣑ic 86@QL7%F+ [e{ra09v7zW 4}蟿\z?b [\CCb>kwl9x"GΝwYП?ۣ^TP:WO-9a]#pC}ULQϖ]OY6`9ed)-olyؘj>oJؘRSQ!a;ͼB3Io¸]ϼ~zA0FNfW1ؾym|$SR(9}F8yL]z!6UiIT [CGsE`YAQCFtu*m"\/'՝hI"a&E뤝KYD6;pKSl'`'%j~aksE[-QdX_%3|,%sLg%(Aٙ.߾`ɫ`G9 |IENDB`PNG  IHDR/*#IDATx^ՙkHT[Kqn",SatAu~XC HIeB$ "D ZNiSv[!jDV 8?g //޸^|k \avq@`G\B\<#q/)mͺĵJt&B%EҮ¯2ss-a{?wXKLI[Biiٗ?V;oNixEe|`֛eן>o.^.*E?9౩}|t"ٱ-U~I};q{>8_jߢ5&o"ʿcRoa.(R$䃧z1coG Ei ߐ7'-tnoBB~I#ImvOFAof.(y"M|KCH> y9$!e;rEMಈ!^9V#HEoeb9h*2"2KbE!^^)>)P7 qernS<>ycI!C䅔R> y=QI3sAd $&eŮ7 5/ OB^g;uWWb]M蓐?jd6"NJ$_O\>Ůrwn.*}ӈ1D5 (N蓑כHj{v6ܬ<.7;l9z2_Zï'L?? + 6:#yYҠ|IK&׬KcU @]ʤ~2NP Zg&.{޲wur  }cfK8`3 Hk做ob9XMG:72K7O?IIENDB`PNG  IHDR+**e-IDATx^՘]hUfVX|P]\4Tf4j ꃔ+~ ֖*Pf楦s7惔dY=9ߏB^AWP{^ߒxأ|B%b@m@"  ~*yogMTw>Z'c?~Dh-l:U"sg#rj1ݡ j"4wք~Ywv ]}Dlۛ ںKYDd-~xw՗_(&B7?Ԥe=/h4ܜ߯]g:\aR7 s<Dеk;{H1@t6"Kd[&H$U ]mp,eϢ-*-gmcIv@h W ZS]ٳD(БTDQX]:z{ވ ɻ}7`@cu)jGñukRLܰW[ Ϝ] ۰1e`ÀmC?\ ƴb[/.YlΝr%Hf*j=Sۓt:^ѫF}P3i; "c= >zgV_ДjaߊguFΤqeVbdY!g*ϞXy+?=.:ӌEI\biF0r.V(4cHrLgy5vR֭<:^vjjOLdpMHLO%|Efpll t[մ:H݉,_M&'2|<;ڀ3&bkH,70;™ HggwlT9wbn0O8< O|TTP7(kw*|Ds^A;<;^ʉIENDB`PNG  IHDR+**e"IDATx^՘[he\2FjPj+`bSҤUQ*Z,`Z}Q@7T'mC$I7&lٝs~aƁ=ΞwvWr=Y.l+=:[a9c@Dh:)<@;aKoc{ e\U=?6$ ͵E%Z,$x[c*j!s#n%fAqY 1Q ^ B(%F@B0 :A/>a1Qs=kܠn0SsBiNjSQrXk֖Ye3KZ%=uwS?(CÅ3Ct_KҳZѾٺkCynp%KkpY-"J st#s ojn1:&M! Y,:YG.@vꚛע2+ؤ;k<$0YO>;vB^ٹ˔Gc3ֆ0ӵSZ;< yj_|[8v_ܻAND]۶XwܾcwWֻ-[uڑ/)/fggK#Jfݷw[st ,rG*:Dgwu6Ҳu.1MSvfQpMau[+osAkHL.O7$?)@! uAz'.RSB bq\To$7;.bvldj̆oLH2!˵Xp75vL:tDb Yԥ)&LNg8H]"4_N==3NvbWzn6].pBMv\~ ^IENDB`PNG  IHDR+**eIDATx^ݕ]HSQsCf4LbRDRQڋEZHTD$H!HYG uܽ'A{Xpk7o=n E/ȱziP\w{c.,@0,vlC5oEZt]=cDv.8.ۇ%^$!$IRPY  )P[ q[&yyEi% u L2k@Cv+5P4}eah0enDzk Q z,;+1br;,?O(-܄Scf;w%Dٽ9ȣx~;:vv*Ł* &lS=m|uNg \5 ¸F&P^V k,@p%$PK~gVf&R>yeG߀h>ьx׸GAfpb௜,6ĻAJJxTYvuULIXQ=W;Rȼ3XϏiB$MOyg} c}9x}(YņBh7B?z! 0c ˮS!@^c;tw~@3-GIENDB`PNG  IHDR/*#.IDATx^{PTU{{!"*9+G bLMKdXL5T|4ZX-kC%kl  \C}9۽E"*sۙ̽3(NX̄0~:wSb&?bcո+FOJa_.0)nYp)Xfֺpt W[0#H=u0N{xy8#eepӳGz郃%CV]rrV%AovbΚBDSEի%QcHN1FFO Aj &ɲTQw^Q 22( Ñ'X+'&:&&$zm|V? Am棣^Uuբ(qc^zDQ"'wΝ$iᇏ[ݘNC 4(yHcdazZ:͵-IK8ԅLa yOAu5kD/ j翿 {.{QXX+D~eEF):; u6C3@&I}|Rڵ?itÎmEϦ>Ǎ<#e۷ ӗ^p9A0qW_o='2oX&4 CcCqSXLwiyr8GJʤ87PqQjiߏ;71Ʊ,ݥ|[n]E7g.+=yTRhNH.Vk>@vu|(QE[gJ[7[k^eXxosXql.ٺ~>{vqt<id`m g=DFJɬrڹă9pthF No_/o+oQ!XXҤ`)q?XZ53YkRtER*˲{9swmhb]te>3{ssva@qCA/mpྡ`m!&}#YRoDrOn\crhe( 2#ia4Teҫ !#MVLè F@*(h74&,.Vh=1KHW&Ο5NPc@UPTc}6'=f&zq>9!Mb8>^?[*\:M܏U{] m&;CK1^>:)X6]:ᵷǖdNgmv'w\cW<鼈YӅayFp!C tTOٙ+A:!yQiks}^<𴰽&GfNc47bxTsJy>##GġΦOmv\%֟Sck@@MH1I/2QQlvj}c?+]xܳ&,$у~*@F&o( `q,sByu]yuOC0jmsN5E7 ?k V{p|]6T_gUrwBA;^V.X:<Kbe] 􏭔:yEc02/56;NNq m+rĠe¨DNzVMz}>ah 46{!K--w ,$($ Lyb4ͧLy;n5gijn&K?r pOh\^_z5'Nb7ffLe>Y8+.\i.]B].`z"U-;WmUOM.ggc8w$XXMI{僙`cN|qeǷ"]ll{)j;䔱32EI`Z> &{T__)/+AǏV:xp?pFh ʾO΋ìɖD=zS \Ȥ;sSug=EE֭ʱ'(,qv]у "I *dx"F⠈!(8*^Q!=Z@L\Pe:qF(vCKח\Q]F4GgѴBy OJ?wܙj]gMΜwTn;8| o-)B9 B@o~NYkU:ʶ{["z߶:$:IV`~a$wUѤЬ"Z-76Ӕ)aG>Ww];Wo_p;Ӱ?Q.J Pw(;A)V A7QwvJ_3zըwi'F|#ujւ;)CRv]l6|v݅|% 6<Ƨ&R7>,قN0~ܒFu7wa404/m((>δDJɯ?s5%YOu?.߀eLx58M68.kby}ÿ x։lΨžQd1镸zwY>3EHXϮe7f]֟lubEH> Y֮uc'QI "20Q$4%>5\K89VqhVЄ;YreڏoQPDr4g7ʠCӸň:c;)<Y_-<轍oVakM鵰4 <+ ,*:߯yC.=.!}b9/k 1%x?w#- coK#Wxv'U9߄6T~"Uty]rku_+Z{Wx{xFF2caN/՛`Usǝ k_xzh>nB%/4k"Ӭ5<\ڝBɅS{+4Nx %vIhW><3֣GH%6"϶:&3npg^v#loXU;'n-KǘnEa5ZN77HH$ .P QzW4:Ԗ*̇,!ó'8YD<5G{1O7FfI:yIENDB`PNG  IHDR+**eIDATx^՘{lSUܶ9ǀx8 YTd:7@xD  @7A 1mjqݾ{^Y|{99->\f {ާ-qX2"DBpGE@@0>ŸC}"B#Y`Z3zDi|A|Kּ<}Ѝ}+YՐyu$~߶܈uoγNȍ:85-ϷmXi{//˚<0@Zx`uv3܏]f=*Ph9 )WyRݻo].\mMg/_K=`w6-𡃌P*+ig #_W-XFtt+,?` BggϘab իeIeԡ&&OX_oo*xZm0v ^Q$ɶٺ.]bQtt4R1(TLGMF,}o?<;y]7Y. 3Z[{455G_B眯$ks|۬dq[} .*9; lK3, 03c>[RB_nTWߠRN?^YV8 e{^_H\X'%O}.[2Wܨ5tFT+dAuFQ׮qLSu|K> tFg|W[:֤"7YBvWoZ.}$K"ðF"Ԗ-$AG<M&`55hZzjP|vUahyVjxcQ|B/VlaUȧju6 AKMlع4W!CbӦdvn]=zڵZ gT@;2kW;18Ț֒jH49683# 4E<Êp:oJTv܅|^zqn@سdAwwi3T|Į';zP)^o!z$Aym<adԐ\W>}»Y}k|YuzR(;"{^LWgSy[l"Q8k̚*Нb;o $31~T.۱A- =mXU󠧳9)JH8w/>#"<_,G•΂"՟pɀa(PYQؼJAF^eyƏsU'sǸKl۝aGĊFj4}d;<X@˓A^j#yopm…KN'$SIENDB`PNG  IHDR+**eIDATx^՘kLTGg\M@QEhV*hPh)K`LQb/Xc*Mm(9gf$$5vw5vw'9s9o}{W@YDo'>yv*D o@DSJ)zR0>YyUNLeKT.*둱gցsQB,JvlUg* _ % Y3ƊzkqZE5q0?6NϺi? _4OY& "c̳Q[si`ܳ?}4. #$>Sӳn9i8>ejh$9O+'zIEV6x\Ѳ h=c f6p%.`dPܼ cO&f7j+˥,0$2wZ3ksI1ahl DА` Oc5 KŒ$aQȰv N;fO2Y<,K\!A rI3H|l$ uǘcs.\łh{pyZ  2:a8ƃ^+;_TD_(H*T$:HO}r  gLy~Ccxֽv{xέ0ethbtwU_1@" i :%5cphٚ1K(/)[8yuZ@(]7,: ׮>`;u>,ieʪ\erZȿ$%ҭBs*УE vJ?ǣG00ʹfwX)yUKUFlzx;kښJ|if'/vl+Wj?kљXNLlZ[*y&z;3ζ\Ӫwio@+++իMFP(703P * &jIT "D+Eݷi V,a zƦfcZ7NCpC0 ܶ4.{v0X%C%مt7]g/?-gf5wp{joۥ[pd=,ò8@H_Mm5;+BW0!:މ !/2Nx= pworJ+`k؛e7\2IENDB`PNG  IHDR+**eIDATxOSAF_\B4J|pAи B"A"h\j܊b(-J(IhLy9I hLM3wgTPaZ-n4=c'̓**n"dGe!;zB-͈=z\d}DǛJz>OiqD뿕4oQTaw>}t  7~ t&eKNB!Fc5ZB)EgNj++=#%Ӱ>ׄ6o ^J<{Ix ydr~l)K-'՝ŜbW$E#Yj #tѬFl,ftt5H+,-omG2d+KX>_bu"=4nx[4^&YtѺbhve(;@JYk}$,6ʟxs&< 9f8l&; dtĆQm!Gغׂv6k>IH/ ce.d Mn&(pP3K( :YbK@eDyC n!^dh0PR>y e+BvL(3=ݤ T B5UAF*EiSl!IENDB`PNG  IHDR+**e#IDATx^՘}lSU=Įـf!aȂ JA@@uCp|hpcPB dá`Θ}qˌb{ۮ{FNea,k_rOΓ{B+ pּWO>H M̀+c4Qe|`HEhC|Bc#Ypߒ5,3m\ѦWr/ȳ-udREs# ⷽ&}fg=n4!@PUU/X(JvGWN|ܸ3o[p}DQsY@JpͲ25.&XSeӠ! F/=~ǤdN3s"iNBj$[-b@@p#kBF wihPNFӟgFqkk O&Ru=A4?^&SG'N-dQW׸1jH X@,2Ʀfh(R56WN8lsrW-ΘdMDCgE mQD=#ӝ1~ ]ד&jb1+gGbhl?ŅRSs3M̜=W̝’t]?uTyRh]CC}mm@w"S\56#:!LN"(1o2(еmByA`wB뉛7!nvLy,KtJй̱,_[8.U~t]vʯPP/):.$f))iйqEFRGeU͕Rշg$ޜ(~DEO=e((,5D~fwh0 ֳf3[e>!Y"&3*ѴlAh͹'0D|#@JPNa]3RI4k’=`1PŀW rƁ |+1{s+چk^IENDB`PNG  IHDR+**e!IDATx^՘yLTGg,.,r6*FmlJSzcTMjڨ) ѵGXc?lMjV uio5v䓼 ߙo}F HjK2ɹ J Y (;"SJI(<,mFLe)3 @02&*)V]̙1FfV+KVbYҖ5yEʌ4x|ȮWTf $pȬ$|2(_RMb )h;S9`<[M9]pOnM鲹Stׇ dր #/f>/ dfmç[ǵ}k}nҝN3%0@( SFIB8k1F&ٌh !D{M:׏= BW%ukq\wޥA8=%[sMn@σPb@'Cl)cn *2]U6[.O.}3 4p"!YkyEޛ뿈q1=[i]ӝ=&& LsMZo q#rDBy ``?;(o N4M{kǏL 2kl='3 vX6Y ଣE[Yvƀ4od\5F@h-qݻX]̶E H!*w]iIENDB`PNG  IHDR+**e)IDATx^KSa?E)4-T m lP4]^K!]fJ6,Sbӎj23ԛ=o~yf(q<M8v ш`υ3~`잃DZl d ؑ2;J(3v=JH-,-9Ⱦ^ %-VXiŵrj,ؠ5-DVٲ~X\Sԯ7(p);KK+ydR ?A~7$"\I098Ǐ;j('N'ÑS+M4i`! *ٌ p04Cz5L"TY;@JQ;S"+WY`ڊm,xIm|%D\8Oȋlz K=K{JAFg.V<7٫wabQBcrh ;j-4B"K]P]7/l ݸl~rmN47<>A xEe 3bx MH+AN)\s7&Ky m'j3R.HDwh]EHo75׊!K!CqQҼ7@*d<$+?R`xaad)xGQW;<zb I0"uDaaܼQBSRd`F %%Kknj(ŵ Y%KnXxAz`RnA_xX`< pY|we BvIgz,{]@G@v A aI͙͝IENDB`PNG  IHDR+**eIDATx^Y[KTa̹LtEzIC"*BPOwDЃDD!i Z1ݬ0"=$tA)ґLǙoe#΁ 6Ylq(NAȐmljߣ-)*r|Jlp LNC @ 2S 0+{z"^W۩Xf*lIM =X }@/--ǧ&Gf v^YfEp>Tn𠫣4N^NRvco;1@}7B58Vc_F^ǷWKxJ)0=Fe|Oȭu\wnobkFYeɂxkM씖oK r AY{*̹y:\G8 )yU@Ϛ ZsK{r|t(N;wT\׷Ԋe24k. r$ӀiM9m@`gQӱPvD SfE66m۵j Z +@ rL+C!nZ0г`)Y1.YmEn7`^4 rreuMy= dҚ,[RgyY|KW!x6zZLDB||0}H 뜁_Oز,ΊxfLrUpܹq|+$纲]_%=ݷ8L , p_DXXE 4CR +GM(p#JdǢDviZӔe1UIENDB`PNG  IHDR+**eIDATx^՘KTQƟ3sfaBPP E)MDFY$Ymk_QҾeZ`5~ ZE8|Iv }`6gΏ}wrD v%kQ~()yRW!4q@$gA@#6NΦIENDB`PNG  IHDR+**eIDATx^=hSQB+ŴCEZvP\RRZ'7"\TAVb,!ъC( rCH8˅~p9hӗۼ/⊬b_`bv4@"85:Ю-:[>tMkdо~D80r-61W}{{1BMR碁 WAtOxaHX!BC$:|*>cYI&K##L=Jgzlhb41߿-~Г5S.dL(n@f'+hz;QY6A}UנRp8Lei<РFhj`1fZb}gwd]Z 1F=ٵS.ue}\Uz5/Lf7^_EK̍VAح ΙIgJ4Cc)GAE Vp Q/ɎA3 daūz1fȮ{x%xʇG; }OT!jZy} 3ٲ3BK5V3{:4gkS2C'06g5lm>txkq\PbeաAgkstixF8u.0Դܻk<#URWԶ[3~l*sBӸq㵊U ?:5d}$} |vTs_XW=L4^=~*hGv-g\v%^Y^۰bi]ܟܭѱq-82BGFfNcݭ_JcDJ:.Y/8|(lTb-ozb?olk{u?4>+<. A0]q_N ~ ^qg!s; n^9.q/b6  IENDB`PNG  IHDR/*#IDATx^KTQ2߆`ijV E6T'RɂҢZDEYѪ6!? )d*#بe9/n 937?9;ϽzgV抠! W:J.b n 6&}&b/B),8#tlDl'R@/-/[Cl7TXnXˡm{a#AŷrBAGgx[j^MʆօN 79bo_S)=PLA<>0Dy+Wq14~pHN,Hg G!1F8(# >T;TlgG>0 2+ a"Ų18k)Y_u,+ p >2w2IP~ W `w Y18zdN_vjd`VBE)e#fTH"/d9:-3&huN7!7q5C4Bt_cP!`mX9l԰VjnW:t!C߮kIENDB`PNG  IHDR/*#LIDATx^[O@irѨ,dD]1QLQ&>b|hWg\DqE311l; &isrf%ЬjB"AC{C(,d{wW}ԆpL[v:=xnèEv`3IDFp9?3F#h?m,QQpuZiKD0-vĽy@x Ffܸd J]H:o.7gó3߆LOŰ0xy.u^mAna6S-%σJ߃jr1LwA&ӻ!>.{5`uuש,.VK'j|\L0 *n8FC\ǹ@W&&(n0*Cw9k`|Ky :eϊfyp c8 ׳1(f>d/]%|61b)o'aj{0b cs Ĵ"Gy))r0DuŨK˿ `QCOMotc}]e{c^rES=A ~2r4_h92*/!q;/|UE@PQs[TCd[lIENDB`PNG  IHDR 4IDATc`h`h\@`l7Pr?^`haXu}o̾ 3n?5 @ 0i (pD{X@Ь @" o0}+^0v G`B4x(qIENDB`PNG  IHDR 4PIDATc`h XN:\;:~@j*W-CCz(@8]@xQ[i]IENDB`PNG  IHDR 4IDATc`h똽cՁ;:{O@x5s{  ^K0I0u4ހ u< T9,pw>a14`n P`7{@U7 K@f, "AmFIENDB`PNG  IHDR 4IDATx^51 @%oI&bIENDB`PNG  IHDR 4IDATc`hXuP?0, -I@T=@p،~9)@ [|9@[ . aXu݇#i)f'@kg0\wsa  @6 n7r/IENDB`PNG  IHDR 4IDATx^Jpj-4|\eGðGa0( 7 [88WUiU%/_=Ji?RrKNt3~~eA:L>䴽6#ru-Md~fawP],:*dI4\AAAN#vqH IENDB`PNG  IHDR 4IDATx^M͡ P% >/cW0ȊÒa rÂ0ֶ `e߾r.p/4Z-*lxiLWꥲUKLg{rGf$M$Y.]j<A<3_-eaH&IENDB`PNG  IHDR 4IDATx^1J`@%1lEU)Pw ;@jrm!|: zhPcQU՚w=Y>γq6׋ 5ۣ(܌n<CFxp/Q+D(9~<4\2+b{mo Q6g[A&:#z%"p v<E"$c1pp v~ʮQWA` rs^ܲPBNІ Jd/n#\HI~ 3k9mJ^ ?e5缃`Yۜ`< .sq -'Qk%j-sNW; 僎 HI*ӻb ҜKm>=,!7Q+;?p)}H9mPum"N=ŞwK@ʌ2}epaYBM9+wƱAPMzx}1iZVEf Aơl:9P3Dp*e 9CR[2폵SK A2hT9c5KGLg.6OB-i#9R(]zw@^ul0HCn(Qtz@)1tKzLgp-;dJPb`;DPe5$6nL @(մu=fKPQ @V'@TtEo{_:(JQ#4TcdD aZ`sWlW+@^e!fgUG\c"ebp4oUѹ-QzֈmGcd.WV7]Iitk b4T؊aYe:!+T{}4fet>UuPHPT4rB";hSmngGؼ-3M뽤9-2`4{P5 ?rwT >C'O֙~%U` Aԝv.16;hk(:r_]OU _\PX#G&G-{Frt167}!;m~D!l1+TɊ]zs=-~[;\=x"~ؗ3:{ܰ W4ha<' *I(n|=~žGQWMUV97Rlb8H;(Buo1~7YO9r皸D3",EaG,w6fV? n*}h*t bX7׏ @~3< p TaoܚL\2hl 6/ v!(q֥7y!l@ؠ7ain\m_ 0ȭy^3~Ѿt:h;Ԓ_ ՏӦU! 6oڤ'}JcV[փҋARQ>Fpw5j7 l-e~ȇvB in9czN9AjO q/WJ^#״j7O?IENDB`PNG  IHDR+**e IDATx^TTWǯ(Ե 3&{4ٜfcb7QرDAl(+b&@9Ύe{̛9:u"Y]\\8:C]v  >(}N!IdoFo>e$`[s+m•GjKp]mvT}e@yzyyΞ=; $H9FEZ3|Mh#u\]XHf󤠽GGGqܵ{sI)))!8{H%)2I3{{ vf]˯[v gp>ST?UtU\\*7H>t9ԁx!E\fLT؅ƻ{jzmP+V.\09$$d4xR>~iiiӞRtN.++9$%Pex踸X㔩eZ5o?:=#`ѮIZAAbc͊ӧ 5XL<9Lr)<^ѐ`3~ ִ;v,ɓҤDY~Zڅwڮ[ɤҴUVi(x)w#2l5x˗TVK8tw#' +**d6|)8xXiYMݵM77pnp^Em%~~r,}ݺuI m6b޼y遁TI&Uɢ&ѴJ1ǏG,;@N>NFQة& MK-~jcpǶ{PDau׬Y:L$X>zaѣGi%GQf 3fLF^FOۖfNW]dgYչK|}c|c}+(G7Up39M?u:<==q90,@g { >M@^ܹ3ovUjYiY0pJxFF`I֥߾ mXXX.){r 9(*,XH%ׇmM .|"ئ.$_|1 pd]^$6++k8XnݤoV:@JAe;~Ū'NMU,ky}s hGkRz$XTEJk8ݻfҥlJ~IR[[+ygl;3@:.]me6!g2አ%ٻ‚ #*6rtZHL Y2[ԩSJٳg#Ο?xee:5[35Y͟،= 0y/BH,""b uVpd=/ԯ\R_P6mڔ7<)b=ff;7nToذACυ$:(\cnC.B{1 8Rs3TVXq=+91`UϞ={{{l֥K@\84Vl!y.usw4н 3DACi }%KlE~b % *3)#9FO gΜM1cF/7m4=wʔ)AZ ?w\8\ #[*N>;7R`Q>%Q"1ua#aRX߾}h4xr$TEpp,OR}3QFVOLrDy R,guuupomWtݤ{NNN VC%+ $K!K/eQ%PS~u[UUuc%z$,ppӧ*`61\ܺsy_ u6%r9dF1I݀K]gZ* Y{JdY֔W'QbӴDڃoecǎ5 n ! xgQάK.h"3om=!7.akza.omٮDmam*O M `LwJú. LHmE)7B=ry5?聕GHka $V(:O?5"`4/\ue_;tP3>:uպb: yd> AMQ/^lŽb>#h=3ɽ`>VΝ;7#(((O8q 8[ /P5uXefJedQ/hYTLඁXVL?^OJ@Ȩ 3+7n "Vth fO9: q{ky(Z-`Jyuj]LY^C~ZH]Ѐ#C$c!cVfE"duj]]UBzA0XAg믵:Yķa-m%ڋS fdrbϛhMhZ =lRjeɌP Bk`f8 d6 k(0b(T"\I;(܏l'IENDB`PNG  IHDR+**e IDATxڥXilT~̼em KYಇ"('UGSE MQA!6cx}}0y)Ǻ~3o޻9;^a<趢(9ԖN"YZmI~~0l6o{$t*nC~ SE5GD!.KqLFo d~^^^ql'NԎ;6QVV)((x++5z2%pxE#\8 't* @5z~ȂO>zhDѤy ̥R)IDk5RT z#3&m [<ׅ@9sJIPdY:o޼ - <0 yN*(d(|W.1#WKz='5&w 8r5k,`h4ƩC)(h MA{tOH\QQg6f[N.'%7_]A|@2 01abu:-Wa]#Ȏǽ!< Mvw\.WJP@2 $31SPLGj|8.\3V__"e \|oooޥK1V>}&FF(Iʸ>p-x׌&aR ƾF8I~'L$Nb֥իWT/ b7 mmmϟ?@*B7ٳG=rI.ZO_qf%I" ^*+W3j p:C^L8N̒8O[nR^^"6ɭ5R,7S֭?Ũ/_ }ă.^W7[lpG,=/^w;m5-l6 F* fI-p(nH&L(F(lųk[o;rxԟMeRYp,7[ WTT as6[<>$z" qs EaELB 8kQaӌJc`cՓ*+ gi1޽z#ǙFW"ݒJ%`6l& 5 W?$ Ti/tvD$JA㊐J&Z { =rQ2y!?b23Wb:T}9G5 չ#^ΆY"7*?%Y+4L`13GzpLHᎩSѨTnPBMf%>SÌZ*ǪH F*AqLm:Aj@Cŧ G #?r> h\sRB]|_ML_GD, sε!,@.01#XpOcǎbs0eEO:}bъ0˵`QQ;ȱ*'O\>#XlkQ^ԴxU R]usP:֟ 1bI8/_dI BOofU2ca "MIoF_w,'K-A.ڻwo'4f%Q1$PHT gsMȩ>hS &8BN +ўSm2]Ç対=ET .\wƍrݻ;wR P$Y4Ll_#dIENDB`PNG  IHDR+**e XIDATx^X pU>,//u@XU&t EZA Xƥq(0JAR:hZ-Y:,*ThDa I&^o/w# sιy|?ñn>$ڲi(T`viZKX,˗=#췫Z+IH$REŸ(xi'xS1u:Bd4-%%iHx۹ks+&!ǤX_p+&iԩSg )O=Ե1c4effAP8TxjHXWLIdRffbE%coٿ=?Yc~~Ĺs禁Ɣ"`)JT%ho>@j H~f٘%5ִ7Z_c,{N La TrQhglZ_koycR #3 \xҡH4\f7tzzU,+W$;ƍiTI%Kbf0 H |9hX?z842ow-p'7T9 ?>HIZ?eʔ~錤$bSn0O2ncH|$hf Cyԡ1=M6jA%V;E7s'LD[Q4SSSW駟~~z@ P8kjj,;w0yB=B ?R%!:LO%Z7(ˊnMcvmѴ+D\0KUVU_kb!fbsܡCNlذJ9R#aeX619rw~/_FY,iAP9uaA2Vh4V!.u]TC{i٥&&n:qU` ; ("i ,#plIK?^/9V-YzI= ]l5fȑ#YER8$_ɨWG3e,2T#a5dOʦ I@{%h֭[`Y\Fzc6xHaXzÇ 2l1R7Z2a1fq͕`serM{w/FAd2lP!G|r4$`rbmEK ҃8Ƣ FoNJ:S FC2ڢp2DP8*)u:NjX/YlnSH $uEc@ mD)>?WVV(HBJPUUy:E%ldy,'bhNJ> *0l -Xhs4}@`g#޷߯O>{~ .l^xC8e[fCJ mjL>ۣSXsLi `4H WPPFc^^ {'غfFQEqz`#XlI=^vm&Ҿ&.E L !5^-0m2jgfeW7l#Y(3왓6R3ΫWh(Ι3WRR2LaF {7Kֲ$\j!bd[Já EJd\.]|>*eo؈! x^_ӋΚ-[|k644XBeQ{Tr&F nڽ{Itp*HQ+iӦ!!mõAH-4qc8x`d)Nww,۷7ݻמRJ "$(ˌpC[:I_|:uʁ>%>eE~cڷo_6,WO؀:$gٯ^,QMWWWƑuPIʪ?G1nMIENDB`PNG  IHDR+**e ,IDATxڭYky{]XvG0<A G$UH!DR))*Z&EJĘX "HQpc@,Y.ᵏΣg{;2,왞}w=;\L(dVUkERbZ<OIQQQwwu>ov񷪪EJ*%u**4cf}Uehǧ8f ғW?i$)S&L***qweLEwX**##$ _ǣy>vXOXg~>@>0}ٳg 'N A Ţ11#2gffZfXti(lE#Bh4 QS0nx޼y /t:&NB`UdX22+cVscϊFf}τC_c/kzhF~~H@hQe ,3rz2Z8q\s N >/ ?7X~=.}TD mtM蠵9t6a*{\q$w{KbWPi u:Hc@+ Xsaxn^쭪A!{lc=```O#̤ ACtF(%<5g#-K rgqI;De(Xl>lxs _{gΜ)5EP馡*dϘ 6S}jlY t 6n򆺺i.{?xvI`AMY/))_x$X8W_K ( .ijJk4I$^o`i0p=pP"S]3af1 [bŊF?mP*b0y|_```BKJv`\h1&Ծ?~`8v{ޫYo& X#fzoذ!$$ h&Е0gۓ=w:~xo}}}#r.\b,^oM3gY,\4oJ$Ki[Bo]Ǽ' d:-p RtEԻr:'phL*\yٳ*hm\SSS}޽{'N-^ldoID"C[ Ǿ֟VUԁ` |جOуXH%rOq&@-B `rooo}GG対cL|7n]x a8n2d &k<7Px4z.8zWX-RնY"DKH㈗J>=2״;wT⑓'O>|6P3i g ;Uf~ ɣ7.p3ة'BH3g^t .1uTsPz^m ИYR\4n96 's2؟@Q3uww߿dJs:W~~@dY)0]v4k[,3FaX_ ;4ZRRROh3n8-+(i*}׮p8wOdԓ;yjajfc.@6[n1CCC4]"yPemy3HYYּ($IH@Iɸՙj*9}ێ; 6 PKL3W(I c* ʊ(׬>"͚"}[oThH.G3jqb,w:X23ѡ@!i$QCך$<%­WL$`R7J>br\\Bl#e\aAGxp)܂˗/ǭ뒋vd9EY#K°p,pldܥ~OIO}G]vF䩬YjU a3 $ ')L*1rJ"Z/H-VH>EN#Eb sjs1rEz96eFiކl=tM}kmgٽ{wikkʃC5e͑HbjDa" mc$8K rma=O=f% @C#5k֢kNGC,M8ujáp/ `b@4!9 6*(`3h7ׂ ,99s͛7vF_ &qҎL<%МDh/궗;њ@P+чQŒr,|' ɓto@u<(m}W=łe ~1!'Nѩ"ʞeq++ܨNQp!Š`}T3 ׎h8|\Vd8 L?SU^ Z> 6遖̉zD8|t֭1D(tPٻ( (K YkY )OၿwCE18 ~'orJw( ‚-~t۶mPb3KҮ"aⷻp>tu+aUCrbQ.4۷o[@ԦժaMޝHۚID;:~U;f mTU])UHvgqLDƑ]R>A`O86SKKWc|C%c5N qq,Tdd6ӚLݱQyA2) $i:V"lܸQFApPd I/h^wL$xy;ƞG_4G 1hߩSFW.YDP+#XP Ew &~d(i$yIs@/$<^?~x,X¦.K)rav6b$OFmJ'3?l8ұc~a GO.8&8@B.L ʩs( K):-@*XĽ8D@/o!IENDB`PNG  IHDR+**e eIDATxڭYiT޷Yz`Ya! 'P,!RjXTRJʅT*`RјE Ea~eEeؗz_w~NOw۳6Vp~ss)=eYt:M%\RUX,RdҴ^7C\H$.R i)}*-/ zfT* p9hX4bj<+QF9Ո#v=VRR諯ZqJfw85Z|9-DBdb<ƢȍH$t8 LK!o3>-@XlSNkjjhL4i3Ix%TҵJN9R?Ó qe8\{[BL/ & XhgΜ9fΜ9#GtIxNB`pd2d>2GEKkL))-k ݮg~Ǘxb3y`0$C-2:σAڞ z/CzqʘhbBLqիeuɀ6ȃδ(WTZbÜN Le󵫗Xh|>%+il,x5tN6 ^0Ԏ^Q[[7h4*eXPw,Y-//K/ ̈7mt`dN/Kk4H?Hݮy>dseuJV@zjG(l… KǎJRG8L`&ܸq>,:N#XH]EYy9ٮ s܏Gnk/3τ]EEIk26{0ܧ>B䆤o#T^a(h _.FjVt0r+I5HH'I1%iLeT*׬YI_pd҃<"kPH|Jf͞d)ɌZy}86BXф">(xS]] lS۷o7o]}R[*g(D1hZd *Fw2B_Ǣ, ' R9 Sk0EUU\{po`0VYMXw]8 L7bޓDf*^K\  lK(0 wJ<8igeUҼwʕ@ ( Ъ}+̠fɴ"W"r ׭+#-mܸf?RnPƇP<*S&)wJ%6¡:}.XJaVM6z<FP XI"vBkeNaj$ FTnG𤆩P@ Ff EUp*|Q1lSEc>%J:|ޑhTA%UuۏJm|pȒX,u5BGE@霎?tMz\G:;ۚ _k($~GҨb;*[%)† pV aW" >ƢGmu4쮀EbSGRᏱwU0ل(7Ҁ䝙qvО06DSW2g"IR_XVdEUi=3#4L pෑ0b6ļ̪* =v}zkkk3S&L z gwzb4؄ZaL,Y6ɤNgC,m4̟? ---1XT4 Y3Y9dk ed6@s~t*13JD``jK *Vm˖-K577f G*U52D늖)L/Ԝ 0+Wwk*5E>/k׮uÚC H\dhK ^E-39VWW\NOo,hO= ~@4D n Vt0c0T%pg VPe (Q6&h_$i;w.4tHTt\* ezR59E)80;a͚5K`ܹs߿_ƇK. CȺ@[Č51 +ry?(f$⑥0~b,pk6lS{.z}vϞ=U%KKG(\PwyRr&t:GN*,zk̀ƍR菆@(}}@?@K#4 '9֭[j dfP,L U#J pК+H R.q@r h&=W z^ZU{MNĆd"(ύfâE !'];- F)n۶M%N:k.̔;:t1RCGd"܌0xKlB +.Ć5076/`R^"q0BdUsQkۉ O8ݻ ϟ~ɋ8-Ո";/9t6rUZīa7zpheP|#]nkkG a[Ν; =>,:W#, үei6sU \nG5ȞJuk[Xym0[S~m[m:_b )oVZ0T*D^!CXknr蓀Z]Fegf%(˶x<;3gb.YL΍}KzE5>|Ba%`anrC^37 H~i%vBY<6z.4v,>gH16*2t|&Mn7)7`!|.׮i ix\h ę=`M A8a AV@hӸi{Q[Rxr1>Սd\A-1&\(Wg#ٴ IZh-I!Y׽cxN\vF8B3 *hl##1<1jiCM )"-3, 2୘MSlxܞ 5>.~ b&e4}oɤO KEU ~`0w<EKMĎc8 Ɇଝ9&ϫjq3%@"Œ^KHd^ c5JV&>p8S)>.7YJu s:oz$] S'ǬvѬ^-A u+l  $N9Git^O%bϺ1pw$EOE#{C{ u{4:4:t]ċ$ o>4io0x: ϯx=yҋ@Q4dpM4`J,: =q44 WF S[W|CR^+Fzu+[ `ޮ|gWsfXo⼠ECI2[ CTHڦ+$Kobr .ך[31W=Yx}hhel6xng y֩_h>ۭtvuo5m}"w/)a+*f2/n5MDV^|[Z?ܓ#np(C-߲hJpB>g`.X[V8HI)<>өtuu?:{V7|FE:CVf,A`h{- ;h68yux8``hXq85г '-T{[. `1sSoo*r=VV3r`r",V l,[WN[gU&X,TM#gXOwO̞vWU$YXl6SuTp+ NW ph2vWԱ"Pfn钻z +pNi\2T:D]j D:'J̗5T:\fpǶ^~ k2Uk6hR l"mʊ_bou60~9`Qt@ cbR3-/D$c" qIU̫x?iQ\)HwpzE )9Q$gIENDB`PNG  IHDR+**e eIDATxڭY ~==˲ ri D025@A!&Qc ABTLE%VI$h cT""Gxp,x =ݝKew|O}?oj$I,wKm3MHl ͍߯:qֹ{-˞=!$4mBwݡp8, kr1b 3qBb> ,LsK(`qPt6~|;X6hCPssJM/#G Vu)rwT3*L۾B&jd>7[ k?`~UtQw$bnWv/[m;9,2s]s+*ê`?@sc`J4KEq" uڨ[TEb!8:;Sa{Rv?tNIi0Y%)T!Ri1l9CHV\|-K565vߘ5VӌB%КzRP&r;yJ&_;jhU8:A VYv3 i_ފ#Q4/q& C +efa~FM!@5ժa5E~'Y2e> 3p,(stdlޣI(iB/HxskǕZmFLg9J%DAdn6:>i2~*\j=`u `FP!NEQiuʅ D)w  #f֦ ǕK%c.HK@NJ٣g003""]zDD E|,N0LJ!Z%&R<*zWLe HUoc S)X4*]T3cp*IUubW?XR!7V9n.عqpo4:fx0hAui}yѱ.efeU), ނ"h)t4_Cp` "<2(k )!"3GȷPu|o2k&;FkծXLŮ̷MX (Q R2]+(fFf2 [;;šЍ ^$NC1٩ȇFj1Q+ `Eѳ۱t I\ >CLOdpTCt͡-\aU?2W# ,`1l f&>6 dI:c8XKOhv^޾ݓ1wt*Jxp |)_nyW{2b[rƵ!x>-kmm x6NS^Ia7Lw:p`.N^W;,G%b?~F5y@{$w5y!/ Gb{{rb40& $|5'fkExڴ7'?THдĩ,:ZTx\HnA+\s/VMRFz^{``u.[gM{4U73{i̗2̌t*um6({]hÙLASyޚ:u4 i{%6C9iR6Z6q\l}%NTQ|>sTʏ7uEljOϭ{{Q~j`栩|yz*~&X'F,$26Y矙tt2ZL2"Ϫ=&҉~*;D*߇@G[%KDk\-FQs~ĵo5BCq>K|>z\XԛT8Rf< 5tjeY,, Z_E݅499Hmx-ĸUh1d>d}ޫkM׎bZgSm A#IENDB`PNG  IHDR+**e IDATxڭYy\uc7ڝ٣{cEB4QR4CPKĂ&F!&C4H;mb-"%r(mws>|hgٙBxw|vvq%<1gj$/ggtfޱjQQ z˫l&T yf6y-##c &S첅0K2&] L%!Q))uO{PZ0lUP`ۿ{UPTd^ൈ7OL d %SC?Gbjf>T_wcR#pǵjt6steXܨ(2lbrz5C,Cض=Ҟg3c lRn%E4ݏΛ7(#淜A8I%ɍj8ATr?dZ+|iK@jl Ut] X)*r( :Ph&.k $nL' Jc,$\R$˗ TD&^U*2^|7ˬUmNd9J 7rs<1Ai_Pth,}޼ QG99h,TUNTx\4br \PsJXQsǍ:Cy z7Z[%|_VKQ Ύ8lLo\m˫CJe3piSm]7OQf4C&])2y)U%;2 `,$+90gx:y d{yPd̒GĝS4 | h/d (.zxsgAӛgMB⛆jW~q`=yeo!&`P9f BdGjC 1D<~IyZS*i鳖 N0_J0g ʉ_tyϠSvS`=Q3JJZ^CeO|ع\^rH;9<~)>G U[iŶ(ssӍ^ Eah/3I Er 䭝`)q^[Atж!qŝ~-`HPF et6ÃCNܗJj9_rGb@]~4 ,V`xb`Re,BDJD-$`$#ٿ (b:샂eG՘kML L#3o3`}gjI4Y Va\!:$9wMqXl{AA ޴Ue%afaw~[VHįB}AAYu,hгUvoVӣu`[ؖh60gJ %B5Oe&oUƈ.5kL&W%]`5|BFA\}Sx#zҥR5jOtRPg\F-[a h~"8OB|:'⇀ge3 ĂS'S\zNkX-xsj޻FFnLC8!-岏"ѯDb_lf=il{mϵ 8nj… 7KԶsxT,<寜:J88$z9d8xDM!2L?dSX"E s2 o,5qƥKG'%ERtL& nLzNh ջ'RG.\UíӬȔb (;0 VUS~./^r l޽{OT*\bv;X|7m}5iԛ9ڝ3GBql\?宜Ig]U< (GAOnǖlڷw{ޛ̟\~bbl`%ESSWG",Ji7N=bMOov?D}j(C?ֹv͑T5ڗ w?կ[R#db8NeiaHl&>]P*s]k\$1uM9tOP:jQuB>Z c3AMg]w![B(b:}dꌛOso ׎ZS7kdI٤ #mKm$f]C3RP,G*6vh'V)Kx>߮ XS{B ?F1,IRT =pGKehAb@RF;}LjIENDB`PNG  IHDR88;IDATx^?1qKHX[P 8 +pJj`*.̄z# zOɒS}("f&$.94"xyx{y1\l s .a>T'8={|Kl5>čgBXLT0$Y!·B#6t2mKh7z$V֩ݟ*gY~vE@,---E@ @ @ @,v(Zժ8 (<|e<͏ճ= p-4||ONrF\#H`W#$Gy€ ) S8˦k)dQDe +{,bFo?߮@fIENDB`PNG  IHDRG/IDATx^10 @/R G.̌H.வ_੊DP2 2 +7Ā81*kb)g6G r Np/Gpcp {r&. Ǵ;[OGm8ӽ3rt+E Npk6XMNϕé@_mc-ml-%=FIENDB`PNG  IHDR&|IDATxڭ9 @9570XPhAAFCi$p;lpt‰^آ7<xܟ= S2Y$"f 8DI>+߇LW ǵo:ִ>= x&IENDB`PNG  IHDRJ~s^IDATx^ӽKBQEXQD&H[CK4ED2Dc9CP$DmHPaAz@S(3c&K;N ЇeLxEPd7R䨑$A )NƸ&'Kq9,!3L3#!2H\ VG+Pe@dxYB l I(]v5~%}&Bo`yiN Z h4 eXS,fy#J[%ud; ąIn&W< x@b *xS|BQ YV$dxǍ6 Ff?)ڀhds(>IENDB`PNG  IHDRJ~s_IDATx^ӻKQ7LZŭ!Ab-jPCcSej(- J.'8{ پ%BG782M1 AUxHJCNq25@/a$AAP l/ifp`B}@A80=6 4D8A*bQ: jEILErX9`1C)>J3OXLVC29n@I4xK~z&kW< x@b |s(3,C̲H)ۏ3߮ q)A̩;vIQ`?iOi#IENDB`PNG  IHDRmIDATx^;j@ĺ>rl7 8C*'1IaI1,C_1Dz0$g~g.O4%O[ eWMX#W y)e'S# ^e˽L`N2^^eAa2G2&0Y`lـ`fhr3%Ffwhe\A+_V+3"P}bP[3IrFL3LV'`KA_ <=ނ bEQ[3Jkh]?UBc*IENDB`PNG  IHDR>1>IDATx 0CQ׫{ؓPnbY#PD[$qcnKje@ IENDB`PNG  IHDR'O28IDATcrh Ϙ;WZGϟ? ?3? bIy D,|IENDB`PNG  IHDRfEe&IDATcth/X )Y@n5  &LIENDB`PNG  IHDR'O2:IDATcrh =Ux$;#ß?$~~g2~30}~xɓ@d)z(IENDB`PNG  IHDRbIDAT8c@.dJglpBZ@rЪ]6s^ y{;~GkUh0ilDZ=>i&B IENDB`PNG  IHDRgIDAT8c@.dJglpBZ|@֯=p?}t{1vM&}}W€Fi4Le /{\oIIENDB`PNG  IHDR]IDAT8c@.dJglpBZ2}ۋ5t/srB_ w `_0MH+YJH}IENDB`PNG  IHDR |lVIDATx^eϱ DQSDi)RE,_rpyז)pScKSU Gj]SЀ2dp#IENDB`PNG  IHDROU:IDATc?ȀPBlc|IENDB`PNG  IHDRK-"IDATc? 3s0!Ė PK2 }IENDB`PNG  IHDRK-"IDATc? 0s13c 2`4H@ QsIENDB`PNG  IHDR@q4IDATcap7 ̀_ G `6!=IENDB`PNG  IHDRp::IDATx^ˊT1JF\8AAOO/(*>C VH](=ݽJ%  #!!#\:ٰaౌ1a c׌1a c01a c01a c01a c01a c b0FbXo 1j f[]ȅadǻ0*HظQ  E2H @Nk̎ʐ㠭A 9]3>.1ŨHBZ'|ZDx=BeDiBK8\$P:?9ho)Zj}f { k Bn㌟Scysb cByh:^q?;^Yx[ᯈZY0X5"fIPPQx1 ) nH ,1!kvpŸׯ.IF BHOۻ0H." Ziw0<ÓV=2˓䈅!E9 D[YGSԕ h+9t$#$хI3aH6̄ )0H2YQիXGk'FCH8[P nX誕"a@8#P24!)~S( S@cωvR ƀ}w"fC8YH0C 1 w(sLHa0CA>FPQEMcobA)1 rwc\4ƘedD&01a cc c01a c01a c01< ԭz4IENDB`PNG  IHDRp:IDATx^A"e_ono畉#OOD1c01a c01a c01a 01a c8101a cƀ11a  T`Ie#F^D 5# -12d()5c3͕1@fǝ1!#RHW0e1H!P=̎;c<("&ϴ%eLc)žc22s}17}c0¢2Ƈ/GvAF`]-~I/}XuoF"yx'! u/h! y& a$o?CҌ ;ݸ G3D$$'%Z~kG K.S^s7 [1FLB"3&y5 m`h3(DD` D$RZO2bukvc8uT4w `^F9*hpޟ$0B(&~OJK!x D):5Cb{A)-! ߡߒFO@1AKXMc1A?(0pn(@1`S bQO'^QDD1)FNC:6ݹ_aU1YAcx_m1a c1a c01a c01a cpA-nIENDB`PNG  IHDR@d`'IDATc@" H2I&0 f1C!  +t>/IENDB`PNG  IHDR@d`)IDATcd(b_0@"Y4dq) IENDB`PNG  IHDR@q'IDATc0@̀L`#c0øHF!%aҜIENDB`PNG  IHDRp:kIDATx^MJ1TsS@*N63>}Fz@``````P1 0 0 0 0?UMh" &':Qv #O1t{quglC8{X)0!COnhseО!P (U2bqJ 㼦!I&GQ!sɉ@X8G mIJ,1X_N9P;C3W7S(92YGNэF_M,ZNm(Q+(aj9 (Vs1"yx5]!n}m_=F -NC%" nx5Wd~2"yڭר*|9Th|jm3_edTZ# ,%+ah3=m"0wb3(*a8W']Z?S64M /Kb8B$C(U=kM 08 ````````/@ mƉ<IENDB`PNG  IHDRp:IDATx^K@̲fρ ;hZVTt2:nLaH40! aC0! aC0! aCP! aC0! aC@i|2]3/k2EҎIX I҉D,)1#-) &9c_f H >2B #8ڡ"#G'0} RCaK#68pKY.`,H.1$fKGY&`,`;fp hB "i&\[ `YnBaՌo&XywC3+7%=!@7hFLp(p>almA#Vu#́kshGǿ-;9O;{h+AscxKoyqs^k{s[{g))}{s8v1Ktd018n!@f@Zt?/f (ҌxJA?L^ 'gp7ϽxDKΞ Z wr3#hN3IqO(Vgz[ZX2P٤j F b0;5!;k5p@1Qp7-WREWNrҫ# agP3! aC0aC0! aC0! aC0OG zIENDB`PNG  IHDR@q"IDATc`gfebfB&!JKqIENDB`PNG  IHDRp:IDATx^1B1EїogiąOm醴  9B=,Yr_>&0` /  0` 0` 0` 0` 0` 0` 0` 0`r5`s.M~٦̍Qwvi}5%5k+-kJm~1G[31VfevU 0` 0` 0` 0` 0` 0` 0`CЈ IENDB`PNG  IHDRp:#IDATx^׽I@p`;a76i bn"p퇰M䤞󚖾fJ? 0`` 0` 0`  0` 0` 0` 0` 02a򘧴jԷ2ϼgbשּׁJ[}S|eVr\qH-=v3~Rڇ21G`=grB*76BӺfo 0`q` 0` 0`  0` 0` 0`bF IENDB`PNG  IHDR@nmIDATc?H@l@y zIENDB`PNG  IHDR@nmIDATc```b fb3$ \YOIENDB`PNG  IHDR@@IDATc?(A| ]XnIENDB`PNG  IHDRp:IDATx^ @E_(g/7Q&G 0` 0` 0`C0` 0` 0` 0`C0` 0`cTҩeU׎0jt.ѡ]0FeW&8`|9` 0` 0` 0` 0` 0`` 0` 0`  AkIENDB`PNG  IHDRp:IDATx^ӻ Q Ekk"% !$䐘O#~eRgN0` 0` 0`  0` 0` 0` 0` 0`X"u|Fnt1g3#8J`$[gLL` 1 0` M0` 0` 0`  0` 0` 0`(o'-șIENDB`PNG  IHDR rY)IDATcae`cbP(PJBQz[GZIENDB`PNG  IHDR  CIDATcafb```a0, b9`f0%\07AxΠda2 &ŵIENDB`PNG  IHDR  GIDAT @@'q 9 a!W)hj+XY>63=$ $ @0/,x\FS+ ^7IENDB`PNG  IHDR IDATc` |A  g.eIENDB`PNG  IHDR gIDAT BAy{w_PPa߁ ~VkQq@k/{@# l1l64$LPh$$=EIENDB`PNG  IHDR lIDATAVD+j«x^;s0Pp`4ͥX}\ݭ@9nXtn5$)4_1 I2# ̇2qIENDB`PNG  IHDR #2sIDATx^˱0ESTJ¢iЛwoH O#^d$=,(H/# v10 ♁'# -Ň bOKE!#e4!3RAs@1GG|1ptt.bSY9yL,e&&MdyI ~IENDB`PNG  IHDR 7T@IDATc`(f b_}W# dIENDB`PNG  IHDR mf4IDATx^A P ?gQ\@zD$h`Kn>w(LʕQ #;8Ac '`NPiƚ-y ;zQXѳJ@)/JH8?BВ7L/ &IENDB`PNG  IHDR 7T@IDATc` |W1IENDB`PNG  IHDR mf4IDATxڭA @ FV+P/PzT!Y #@KGߖ7wa &+@@ @4bRPƃf'#=-+o'6̡qY8DY{mO~DIENDB`PNG  IHDRGL)IDATc€du_`_pŮ{i,IENDB`PNG  IHDR9f)AIDATx^}JD1 Eo N+7.1~ݠ´5 K CNoࠐ 'sܿP st k),:Ab_DS)!6:ig-٩z]Tz'+jɽ^>(ӢnQ.ߏfƒd|~z{DW^Rvj joFIC,_ ʪSY&3YrDkSnHIUՋWPJ K"+eMȋ+>+ȧDjʙ+3 Z1D#N9'D.%*TVJ"*Z+Sd]^@ċ_sIENDB`PNG  IHDR9f)NIDATx^KN0;SSD7x] ΁'ő])rBsĔ:^8Edˬ03.ksv&"aM@[ƕfp'C3\{^OoӸ}o~>F{V4?Lm?y2F_v{ܚlZST=>3w&"4WqV@2~`.` mY7Y٣&#QZ/?`ZdZp-\8McC38`#hLj'9& K̏_?+ @IENDB`PNG  IHDRcg'IDATcd`B& f ,P@mg-~jIENDB`PNG  IHDRcg)IDATcπ9'b _06BuIENDB`PNG  IHDRt-IDAT[c?02&,3`A%XQ 68D0q&IPIENDB`PNG  IHDR9f)IDATx^AJ1 &ō .^Kx<.J| f4!=L;H >f+ >5 p a+PPp|@x}lD$dX Sbn%5~]ٰ$!+CB p,%3,ЩGԲ -p&`~N@.yA2 K{s6>t7J0:+F vb>W5¿З wa} ?M۲prq}7(IENDB`PNG  IHDR9f)EIDATx^;J1 pJ ظb= sK40wɤR0&[+-DF7 !2daCMG΍a!:#k y,hAhr91$t2*.فd OpE/!76-D :@7٫*Ys i.H#͸e.2Yֳ<>k5`8;IENDB`PNG  IHDR IDATc`| NX?OIENDB`PNG  IHDR LIDATx^+0E)B;<S ܣ{%Lv,Ë6fI5n]͍^xSV%E6ZIENDB`PNG  IHDR MIDATc`%BW `PJ"sXA(@9#0 ' YTE" r%DuUiIENDB`PNG  IHDR #2sRIDATx^͡0 &B/'@ɼݛ\kVCWN$O0-=URھcTn8mʨL̵Wv4tW|IENDB`PNG  IHDR;֕J|IDATx!0 X8>@S`**6+g &fU1raV"6 ۶!ր9)%xZ;yODt`Wι7k+bZȾ~c\Bn#0IENDB`PNG  IHDRnPLTE8k8l9m8k8l8l8lBfNZUq}vfMR7k?4B7N>4@5 Z;0 YLS [Oɞ ]`VJ?Ԕг \ _ Y ZeYG=MKhH^ Y XO ] [LIIY܅D:D8F:\ W W:wҹ?z kXHGFsC8A6˞"RVV.nH/RE=xډZB7@6̜ ^ LUU-m;w9o͘St?5/nɜPTS8t]7nɻGOLAǜ!JWb>3=3rlҮ,QmLc=2<2<1䰬ڿ\],QPPͳQJk'OFyG YO:/<2䳰qVN0[ޠϱȧI|MEy _A~=vI=V2u] tRNS0IDATx^c$Iྻ=_D6kl۶m۶m۶mˬy;;Lߋxs3ژkE8] 1j"Οho*O@=Þ 6\yIYmE}!lϡܯg7 exܧ2k }f.1=ZAN-سom&<[.=ncj=!!$jp2qvL v8` O\sYw=MhZq\qH cH\H1_0<=,Y|;:a!zS̄)RBl!f3YyR@2jf6('Q@)Tׇ4\ׯw8GzGy==JaT|GeļSB'=cG` PSxc-v‰'|ʩN;3:s<`83S!*i*E7K@Z2$ď6W 0v/^yմk:O. <)ECsB*Hu{Z?0Q# HE4!}(JHd_~Í7<>;ﺻgą)V%huL 2E B"J9O$j/uϽ=4@5 Z;0 YLS [Oɞ ]`VJ?Ԕг \ _ Y ZeYG=MKhH^ Y XO ] [LIIY܅D:D8F:\ W W:wҹ? kXHGFsC8A6˞"RVV.nH/RE=xډZB7@6̜ ^ LUU-m;w9o͘St?5/nɜPTS8t]7nɻGOLAǜ!JWb>3=3rlҮ,QmLc=2<2<1䰬ڿ\],QPPͳQJk'OFyG YO:/<2䳰qVN0[ޠϱȧI|MEy _A=|I=V2 tRNS0IDATx^c$Iྻ=_Dkl۶m۶m۶mˬy;;Lߋxs3ژkE8] 1j"Οho*O@=Þ 6\yIYmE}R ?׳oqu2WOvP5 BPJ} )==3h3qjwqvunQqC KB/LgZ'$RY5m0GI f`>pu{};:xԡ~đGC 9T@9Et a{,vsx; 0[=G=b'xɧ:89*vy=K/:e_Yp+~ TJCB6=z5_^t%^v+v͵]_Ɇ!|ㆭ*aZ0TZp̠0(кkS4r'<3vt<|GorPV]d P=%r 2Q5 3z7`r@Y0z{^x_yz7|E>JA+L锦@X}N' |\ϰg?l_/H7ΡE.~rIENDB`PNG  IHDRnPLTE2d1d1c1d1c1c1d:u֩_F~ٶфS۝kOّ蜴qxaܵRM1b Z [@5 YS>4;0?4OɞLB7NòI=ƲV ]J? \ _ YϺ]RcOG=MKcԳHV2 Y XF| ] [LIIVzwD:D8F:[ W W3q9t6m Z _HGFpC8A6˞"RVV)iH)PE=xډZB7@6̜ ^ LUU(i;w9o͘St?5*bɜPTS2n[7nɻGOJ>ϾǜBwWb>3=3jcѭ+Q`ӟLc=2<2<1ѝպW],QPPNGk'KCqG YOЋ:/;2РjTN-XѿÍ|T@tM=qɾ8qϼhB tRNSpIDATx^c$Iۻ=Fnsfil۶m۶m۶my]=;3v"2_DbÛư /`bGŢE=l8ēK 7Ͷ gгfk9=XjLiEn@a^xzGL;c=zzjLv Wvl> $nU-%D/ U(kF2~ƙgM;s; /j ]b#ڪfrDdRTRtfS7/L))į&=\խ^ke_qUW7x uyAYAR"$=ɏ=4^)]CЪR2~nuĻ|Ͻs^DzdJT']YCf{x!JP4*RcG@З|&{;^QǫȄԶPƼ2$W~5`u#}M"[#\oV_}i~;}?- IIENDB`PNG  IHDR;֕JIDAT(c?b@vK;3 ?G̤~]?o<'Ƀԁ#kfJ= V47Ƀԁ#kfJ=I lȚٵRN~kfJ9r;PWͪ*B3+qaă/@ X%`:5$`6<,m4wWҠIENDB`PNG  IHDR;֕JIDAT(c?*y3 =15y6ykljA uRODMǍA u5cDj>6ۿg"xսЬ&\=n+؝բw`b8P9Hf `b! ÃAlTE ^RIENDB`PNG  IHDR;֕JyIDAT(c?*y3 =14/zn}]I$Rg;UA uTzז9G pa'LIENDB`PNG  IHDR2dIDAT; 0@VBRzIP{B Zl#{v!`ih/4X n Iրl51tKLrD: 5#sfbPPb2B@^GxyMkIENDB`PNG  IHDR2IDATx^ 0E/d_t84 h;qДWB0rt-JT(DN4A\w'mPΠUC.ho' zgIENDB`PNG  IHDR2IDATx^; 0F?S;8/\+ NRJ;cso?p Ʌ,5:\ :ԴhʾD?hcLFZ+-&+>> :84T.#=90 Mj3pK4e{"C<#NZ'T8GTʄ+q=7nzIENDB`PNG  IHDRR;^jIDATx^ 0E+N]ڥ 3EAp\wGY!p )X#$Iʮ{q4Q߻93m GBr8iYP@EQ'd*TrUU4 ~Uo۶||7eP 03!!t D %KDɑxr8s u}GX.H1L\#H _tiYIENDB`PNG  IHDR2NIDATx10FWg!X@UũO4AGc|pˆ#~VIENDB`PNG  IHDRR;^jIDAT8c?%*g@H R 7۳_QnMGϟ? =*ߑnPʷ$3j2͋y 7$leѣWw!}7؀Xň lT)ZIENDB`PNG  IHDRR;^jIDATx^K `-Dm4ʢvjdHoA0 \qDT+ +BaAt(t7 s]09X 0,BX4uX0$AzAiR ,HfJeYqcjfe PL@ePHpHpUK\Л_M9=^coc @bT;_ D2p X6(;JbIENDB`PNG  IHDR2IDATxڍ0࿠ԋK].vAŋZJ,^7ST$ B ʚL2O6=:.[vYSF&,7ҥ,NQc>rNr:G% l! R1c}u}Zi쒧=>4DS:1 <NˍjޣxPjy< 0i3Ęc/xP u4mv-\ }@IENDB`PNG  IHDR25IDATx^NQ&@60h4!D# Fc@BMx:g3ilf2_]?/ *j!5TPg'|<{=(o (K( %U­fDKPPXdgj#.<; #[ٸTdґWґO4TSEzNzT W&]Ä7ґo] YzHWNT(Ymߍ;js0ݭx0 #rsvs rT993Z8c (Y{J%4;IENDB`PNG  IHDR2IDATx^n0EoPœH,m?0E*Nn_xUjXzG= #A*$#0ʳB鲕O3`j^͆ϜbT5=5(SN7Bʩf]v;) l#]"qBڶ]+W|V|鍰šK/<ݴ8Zą.y&y,&Nviaw4x' |BwRGR b(FAIj 5)S7+^mTeu7C^ҕr`<-'e&aJϭRmmGzLZlu4܀sGl64q2p-i ։-$ML1H|')#IENDB`PNG  IHDR2XIDATx^j`ǿH!8XNF .EPl..>/DBDцs.?p ,,"R,|$0Y.moar8XXh‡_@=0+E z>Z(-J\t1ͅȭa W0H 2IG U>fB&&%0݆\ NOD o|x"W3COtM /фjKR6"6ڎra XÃju- XCݚw5qxx,qRHrQ A!QN_7>LeIENDB`PNG  IHDR2IDATxڕ= gͰ3hr)u"ڡP*1F"\DHL(B O[&2C)@PrУГCUZ2/r4Z6?LY@G`6ԂDYBr^7ZvvȖ!+k Z9-lM: y1Uݯjǂ\+o-c#]A]IENDB`PNG  IHDRR;^jwIDATx^JQM wnk,Jm 6VR,\AB0@Al& ps)q}—_ {P*mŠhuK2 8 &VE   ;0?%!3pΥyLNcxT[hw1`Re{Q N] *:^['A`)lnZAWK[%l=U6r+c'sxׯ",:J@qC0w!q ,:JВ M 'WBI ь3Y$q6SKo>MsNS%A(֜_yA:IENDB`PNG  IHDRR;^jIDATxڭT=KBQHڂƢ$2A('!s!Z 4EZ*$lc!(uW{zCؔxxs=^{B \;APBG=ZfV$nj4Oj(af 4x~ö]U4Mh4afT]Wu"t:-n74遗2{\0T霼l6+~BPF"@|>G27\0_P%.r8fZ>хdCMFZ k/2\@Obb5.*܉GxQB 9fȳoo+92R::V GFL#hP!v 3h]?3A|:HIENDB`PNG  IHDRR;^j IDATx^RKkQUbnB.big !!$ PmARiTD d6"IA?Ӗ׌-d\Ւ8u1Oū+S+fuQ_Sh ;>4bO6kspi<?hǓZauq@C^dDLJF_ &y;9y _KןU>"l&, x\(QAuMx0 Q/e {p88K&PHڎ(c!!ueh% Ncq39B{,|T T*-Xگy~eqe! ,bb[8f^Wۂo6z-W?l6 (AFwWf߹DA :Jh4Y)cA NP p8Lh$Sڶw BE"3]T!KKGrrj,٬~dy <\~ժnKLF3x {z V쟞]~pȶm z=r]@c&G}F0oK%8Whp2bgL?&>lP\1wse8Ɨ/vgoڊ?<'\s9ǜeN2orA[ypIENDB`PNG  IHDRR;^jIDATxڥMKQ 5l .IrNA~@A/ZXE-DA I j6ΔLW{`0 ywi"4P5Jw][Jm]_6fuh5kpxK/T"D~5zt&oM@^Fw^2aJu<o=yI3AXPzDۺ8Bc}pЀTNmKp13؞ B,Zw?8ʔVBїt] _f[|8HJ+8|Cq@3/6Q%Ď=y65&*//eZ+,%'ѧRH^zF3xr^05Wʉ \q,pq Y'/1cgb;qSSyLscX-L.kklpIENDB`PNG  IHDRR;^jIDATxڭ.Q-܀, S[H Q;ZaCB"bA#J.Hv3m7HhԜ{424ɜ3s i`|L%.Yhޯ;i~'*E@q9֞-0{$2iFT `\E6N(dlvȞ^LU^qx9`8=u4V,udtM|4&S:Xt+g?\Iq9``θ=L*]ί)HT,ޑ%n }ѹF~vt;BUNn$\UU`9,ȔD<\ۦc`X')sЦ${J~ΐ~j~gضĴ0L;4BJKIENDB`PNG  IHDR2 IDATx^=k@ A"9UZAeů&._A?E4;;6"nB/tpx~sFcx^w-iXtZw0O5h& DŽ$JYzޗ\.^֜ݾZN{!,: (>v{@0n9vgD>A]׎IV lup#tЅc,d _@jiх.,i2f9eB) \Η{{fp!7 ~;Df]8;f0KF͂^o,&zakSYj~NgAY0L~\fB{hƳi@ggǗ⑹H$N)w(\gYt pA J6oބRXW5$@YtЅ.zn!`?T(u TUz wr26Apu_'X){w+`s,.!'^\X^ekk;R\t"0CYtЅX$$ g2xh%tya KzK$6DxC%bk"2Ȣ3b# \c3d.+|'LvuIENDB`PNG  IHDR+h!.fIDATx^ӽ 0 i(A9h;yڇL:E׸_t#7BcKVmgI}md;\V:ؾW`kYv;[(.ojC\s~5IENDB`PNG  IHDR7IDATxڽ 0 De _Ppn PJ{F->/n\МNޚIENDB`PNG  IHDR+(' IDATx=AWYq;k\EtBa-Gp 9D֓1ɿ7[j,y!CX/kh40IYQo|y͍~ϸr;V16X>Xc{>č7~{v u;mNJŶƕ&w(*%tsvk3MB>(p,"sT`n5tu-Wk1ZAǢfA *rk8IENDB`PNG  IHDR+('IDATHcXjfac30NO@`P,dž ~aḇ y K|P,èPYtRӡ4`QǢ;e22lH]CRm+Z/ 9t{c%b `1(x98fvIENDB`PNG  IHDR,WrIDATx^ӱ 0CQ4 0D&@ˢ&N йC'D4ƣ|Ql66<[hy|fgl4rU&tgt<5IENDB`PNG  IHDR, <IDATHc```H{bc;i/Z|61#2_au01qM;IFw$5KLGuLC* bmTR5.,0{9НA`&+)̎-PtR 6&ѠX ;18mHYQIENDB`PNG  IHDR, <IDATHc```H6{?1ȝg`96ĈA`%>hBqt,M0#Xe:Z쨃dRnkCyjBؽ/ ;=ALt`vl%b1A `1Pl<1ı@߮yIENDB`PNG  IHDRReIDATc```b_! fxIENDB`PNG  IHDRV(IDATc```bc fb 00C{IENDB`PNG  IHDRV(IDATc```bc d2$*b0ϖIENDB`PNG  IHDRIDATc```bk Q`IENDB`PNG  IHDRIDATc```b_ $IENDB`PNG  IHDRaIDATx^A @ E\Y7^xZ^PfRPÔLZ>ďd& "HBneY0##N,l )3!}A*eYN,[{/tݝZPח) 8B ``P[` =t IFLWMsorȫzIENDB`PNG  IHDR sIDATHc@db |dH:ЎN\AR=Mx O:{uvԋ0o&0#(0h(zM2 bItsh'N M(?%ayb0a0XP@$ ~C.L[!}IENDB`PNG  IHDR@@iqIDATx^=n@.ؐ.T!!e )/(h "@o 3'}7<@<pVrVk9GYVa#0Mfg]H۶q㱗/Z( C@d $u#7eY#8'E-!Ip83"$ vW D4JIpq˟繗18@F^#Kbl@"|\(H& ~5Apn p7=F raPPm@"@ EGЪE1 ncЊo(j7 DFX&!+Bj IE@]",BI "iJeQ@,BB築&^ 0__H]8IENDB`PNG  IHDR i,IDATx^͔JQG] &f b `.c"6,EA`u{?>w1-ZL eD2I ^_MxWV=> X^ZP!"U 3m}xY8=T5eW̃\|v5"{y@Ia$O.%R4%2Y-Y R!42 (5h0PYAGYbXY즛2![mF03 g_6zIENDB`PNG  IHDR iEIDATx^͔=K@ًY]!(ZVF m,-k[Vb!E]4qvBqCg3cPa0vWmٺT9Hyvrt 13=7Z3ecvAepp4ܣBCmQh@0F ġB*$7Aibka>XDŔ~"USDf_v\&RU3qm$=. ?V<q$`m.ΥRK#/[Y%&}w/oyU/AIENDB`PNG  IHDR iPIDATx^JQm p&7AaQA>,+!  . C%PR*L pf1a in0qYIX$!مӭ{ΖQ1Te#fa>ijU%na\"C ur DטgW]\ y}aˆ=;ңY/ \k6OPA$Gg̴Dgdsjmvȭp tĘ&Ґ>)\pl`5=AQ0r!ve{;bэ 'H#b]IENDB`PNG  IHDR@4=IDATx^Ԙ銝E=#nŨWaPaРH ޔup\ɥ)36ֺt^m@Ȩ$5w4wFƨH5QlkK| p0 f4F"w>nԜU 8V V68S Rw>ZػD_MCQ#7x2e$;po abl ۙ: P%7<,xm!^ӘkcS\ZVKPD55 )߾oUw69Z/• ;"QCGԭŽ ]81;C%b"->GA*fз|uf) x.\ݫLyBWhZ5#o~T%1'd f(R lp7(o|O *]F{4oRAl웇XnfjDz5PbVbqxR/kS+pl **l{Jtج{ܺ}ltn,ƚSUl!TI˜xݭ ŎȯRdAnW6wV+v1GQ.EX'G}:> )*5lQBnp"O[Nf)gVW49#QppX;u@둼tb7#SG1ضmo" |<0Zȋ^Hlͩ28~I^BU~ny؋lXʚRzJ'gՑ=L~gff_G3KbS'˷zCp`v;='q a[r @IǢg7b!1e$ D==$ޠ>}ąsoJboƷ<||ܓ~d7RTƱ \?M GbQÅ`P?-[a*!`E^#;F'nbͅ$ # ԥmp0:k 8vd)Ec^W>!& M?>̤0cE$Qmɓ't8|YlMUM쫻$fF/Ղ 6{`&k:Kn~rl_Ä)!U[q՗Wp3c(vq؍xŚN8H}T\Uy[Y!<{\ǪWΠQi7Y QY[\A.:g 9 P{*f`+- `!}X)3VeɎ=;6c:Z 6i"/V(h @!? xZ0G׮]S$P/k |H_(KBnP 8lI҇iYJIENDB`PNG  IHDR@0KIDATx^֡ dH 2νtzrL0a 3A 7o\(%IENDB`PNG  IHDR@_XPLTEݚݛޠ᜵ߝߞࡺ♳ݟ᜵ޝߜޡᠺᚲݛޞߚޝߛݟᚴݚޟޞࡹ᜶ޢ⢼⥿夽㤾䥿䦿奾䢻㦿䤽䢼㣼㣽㡻⤾)CIDATx^Ir1SR9N,a[j] 7lm>W.Z4ǽ%s RU4wFhH/_YӺQČ]l&†qa3Af3;c.D@ƘA!0B@ Jiu6W?vٯg0(*| ك/b@ Q"_vKekץ7jmsV滽lFZ8Ku@PP LU#QpEInzu= T&*^I{Z.۷X;ck><7~:-3c@QhAvZp񩫅d-IENDB`PNG  IHDR7;IDATx^ő1 0Oq)͒B@  WB9@g/ $B;΅XlgQIENDB`PNG  IHDR7IDATx^1k@(@ZCBL"  Ct(N`LJp>'O?@+wRQr$5 q&Nζ`#rX"pJdE. ]∕`Se2X( B3xeJ fa K-Md|M Jg@4P0Ho y DfR kJ2{c'~f -q%CE7>ߚxqz)8IENDB`PNG  IHDR7CIDATx^Ka?\B{)_p!t8 l(Rsr!5>%~򼼿 ҖSțAZ4bx-_Ț"SI<03ZkпY# Kv4@LFO};[n3#TEP)z/Fq .H']#tf>4<<{NhqT:Fx\ Ė4͇!x2koPD1퍝3H8nMgRNQ$94IDATc`103LB,.v6(dhahfhCFdCC-K>aF|IENDB`PNG  IHDR et7IDATc`0 q $p;wq.H n**ĩ$k8"qcVIENDB`PNG  IHDR {D!IDATc?!HgE@<Em(jAR',tIENDB`PNG  IHDR >2IDATc`1(1h!< 9| P @.,mIENDB`PNG  IHDR et6IDATWc`+1HR  ˃$90'46IbA\:A l̀BIENDB`PNG  IHDR {D!IDATc?!HgE@M"bqQ LrM%IENDB`PNG  IHDR >5IDATc`p123@! P,LAr94#IENDB`PNG  IHDR >8IDATcπ I2@Y1edK1|gd0sY\&_o )p8X|IENDB`PNG  IHDR {D!IDATc?!HgE@@"[bQ L?n/IENDB`PNG  IHDRy!\IDAT(?KaߝQ[EIN*up(Q>CECN~h>E[KkSpc᠂dyo?:̑m^ֺW^$/RD)aOl0eDzDPuR2T* } v#v} dG\ 0qod\L07Ne)y YսΝs{- 9cp+VxM1Azr>?}|<ۓsJq4,d>{TȐBCaBE#M4[lE$6q4T(,6XD(P K{qIENDB`PNG  IHDRy!IDATx^1 + q&Cv8xDU0AߡU c_QR >5N4L:;w%?w2_Qa0K2*-t>5aSa`r]KY=e~'ҝVAЅ5v+iSz?WaqVCd6$saCsIIENDB`PNG  IHDR Sk5IDATc```@ ~Z P`E[ 6`@H ?@XH!d ]^.wIENDB`PNG  IHDRnvZIDATx^-ȱ 0a* ;A\6Bp}bCV ыcf!6"ZP@vFJrJbm?TNwIENDB`PNG  IHDR(TIDAT(c`H/NdHcj@ DeM 31!M@X D =kKq!Qk92 xK7&ܾ#IENDB`PNG  IHDR(TIDAT(c`HAdHcjI DeMO1!M@XD =kMkK9M re|9@&soL<%D}$yIENDB`PNG  IHDR(VIDAT(c`HHdHc`H[9Y$)pT5=ń4DŽPY~"6OIB/H3X)C_1C5IENDB`PNG  IHDRaIDATxڥI @E=8xv؉qH5Q4H@+ WڭԢ#jwL г`xԆou뎰Sߛ2)^i.w\Pt6@ʲ`n|3'PϜY)U3bY6o";S3`,S+Hh"0O" AiG/pDR^g)@_/ 5>IENDB`PNG  IHDR7IDATx^M 0=H?$(nlBɴ)ex XlH0j]2[f.rG!p`I Ϛsbq G0LxBp%>qK#GMG2:# 5PCn,NM[gT"to/Z)VIENDB`PNG  IHDR7IDATx^A @ P4!)!d@+ZpTol8雼`>9Ɔbji$vuuvHvU|U,#uQ>(J:L$#AQ"z[9ZX4S a&:&8oIENDB`PNG  IHDR7ZIDATx^1 1 RB Y_ R hB(Ph~BK-R 7n/F{9+L ;;L a^xIENDB`PNG  IHDR7AIDATx^1 CQ!c2J$_yQي:$V+<A L+2lJ !qIENDB`PNG  IHDRFtIDAT(c@2 nE2r0%E*~TA,bgV8)xYYl̪=s*pbg~)W$5qYa *Uwtmalkwm!w!8L~IENDB`PNG  IHDRFIDATx^A 0FX{' +xHɔP:"`ԣ<2„|LGkphYR#FYQrX3ڈY$֐?l2AFs1zGn-nwF\'^J؂IENDB`PNG  IHDRFIDATx^1 @F"X'6E]C0"Xl,V^6_IA\͑ITC-b1 ţ>/{ oXy wVG?Mf.U"@];v='"0/؇b}+y ؒAjH"Lwy0*e$rV&pڢE3վwf3fe>g:"UIENDB`PNG  IHDRaIDATxڥK/QXhU/3jie7QEP L=.m%>'UF+79s{D$F49ͺ Ű7XZV5(o.'^h\D*O)4(D=u)ox?FKm`T^ 騻n*]*NG# #D`pB9Yqq5`~D9 L}œi==D [Icm w6F"JymN"Zm (kcfq 'ϼɑlIENDB`PNG  IHDRaIDATx^MNPC& s'$IJ ВȯOpא*G>Al%CCn61(0Cx1#l)g!7<!phrqe܉h6 ,A Zc6xq/pF *E5sZҎf43GlBPgD,?[388An(K0E'~j*Ʋ{Fy] W~ꪯz#%.P'MC*\T)P3J f7ľZyRa ]-z`.G}e3 -`$@5¯M^gauX@,! {+;bfLOZ+vȕ:cc%uCmLHTOǢ'XrXsۨWIENDB`PNG  IHDR7IDATx^uQ;nA b{fi^%qqgX )q!DY=l GIN"P[ L3d7&fʌl1" "z;J. g3%m' %؂2G.w7Hf~F0ˣx/aY\%O ADbҭ_ ,7-9L7VI%u5j-7B64C"(" }.UC6:Oi 51м+ׄ= ?ͤwIENDB`PNG  IHDR ,:IDATx^ͱ 0_A**DFP0Q+ԸAP ''p ,xIENDB`PNG  IHDR ,:IDATx^ͱ 0_HA**DFP0Q+ԸAP ''p LIENDB`PNG  IHDR ,:IDATx^ͱ 0_6A**DFP0Q+ԸAP ''p /߸IENDB`PNG  IHDRJ'"IDATc@( 8cfIENDB`PNG  IHDRJ'IDATc? @i8 D5sIENDB`PNG  IHDRH"IDATcpqqZ@̀r$8qI0& '^=PGIENDB`PNG  IHDRH IDATc?:vqq)d!M\D`Ĝ\B.IENDB`PNG  IHDR'+RIDATx^ PBRGڰ'ra0'"-ZEJ-j + L٢9u{.tiIENDB`PNG  IHDR:=5IDATc=s#Q}b20ıE}3ɰoe ݍA5IENDB`PNG  IHDRQ g!IDAT[c+?8v%a KQ‘IENDB`PNG  IHDRVIDATx^e r'vqb*܋`B Q/iA1ς& W$ E17f-A/X;\o$IENDB`PNG  IHDRZIDATx^]A 1IJ#=~B_=t"l,x !̤OGAH7iA@"@ EzO[ {ek"t ?nj;TcyIENDB`PNG  IHDROU:IDATc?H2Y~ \ 0–hIENDB`PNG  IHDR:=-IDATc@AA?cʪw߱uqhIENDB`PNG  IHDR:=,IDATcπ |B;p:RL?˟~Fq# n>MIENDB`PNG  IHDROU:IDATc```bcF v(IENDB`PNG  IHDR:=3IDATc`ɠCp`d``5 .J,IENDB`PNG  IHDR:=3IDATc`B N f Q/J"IENDB`PNG  IHDR:=3IDATc``_) 㙴SC-!w8[#R f j j:C lIENDB`PNG  IHDR:=3IDATc/_ L~Ϥ0012ĩb`PBsP uIENDB`PNG  IHDR@IDATx^}hdW/B XMm6 ht붵[kԵѩmWWWkGSӎMMnXݺϓ.37w%<㜹{=Ļ3 -@-49ՠ B,w%Jhz;ߡ i5MXLJxD># oկL"YR$1Ӧ]ϓw?ih0 f/:66 .GQ4q%~_7DP0RC'_zꂿ7gɅCq)uK=OTƿU2{:wߧ}N(xj$uQh'`?QDL(oMh:_ݣ'իƿ;WpvqOfql3Q3uOw}77;!_7E~#&̍:iߚ}>r~O]!oO>($ٿ~W b3 $lyy|DL^[/ȳޯTOľBD%Jh  ֠]J~4<"§``Ѿ³8WR F4 &4,fO@\ܢ|Oe8A&v?BJJb?iZQ2v!=jȈe}m`2?;koW0>|Z3E?wGsNNhECVƏp殺(` 2<=Xֳ݉.|"iwGMeK\tO[>h #J(]<ԫ}s>ǵDW +.|x9YϽl娂Hc6xs˳A/ n2$Aa_/* @w3ɣwg1 UR)hL;0CJ}-K2y^Р4 >y*2[Fzk@r(,0c/ا(|lG65@ʿď9nhxlVG#K꯯Oi\W9gw_5~DŽ}ǷOr K d[DG%(WK?Qfye wÙ7!\h/@r%c0:\Q̝O,^bmǯ?5[^>n_/M o? p,尟nϙuZirwFc-7ـϛ}>f]˫ w5&2|1nem'|Kɗv>|jadLT4xU1o qN*7 B/}-3~lu~\9иIMOk>:}J> `%%_3~Ek8+1ɦ-|˱&3\-? ]]Jv{j&V49k=]~P5&D~A?IaJ+f9-7c6@8"e<7:T݋R6(>~Dɟ6wI5᠖yeP Y*ky,iee?3}<,ň_ˏXj 4AhJ:ac9yOiVw1IkxGN2 l!09*VLyMPNd|^x<ƌH?qN)M) q-d#JĽRaZ ^[scn wIU' I(},>5xhA 뭅}=P*샏6ʼn(4A ~\ݛL[xRPt _~<c5Gl1݁7,Ycs<ߋ#h"Gyss ;r.H5Ok<Fg9'?ic%k 8r>8p~2%Jay2'O<o:uH0aJ3v@ VӲ^6Ϡ!"B[6_3KS%oy#_!sP>s`ѭoBx/1wG<-ns*m#}ad3$ƣS/O `Ujت[&~3Ɩ5ۗL o_=b *lH猯5cײl`#h'|3@fvz_ cXeڨ2bË_5vIDATcq#h?#+ Y30d3xoq l((+[TIENDB`PNG  IHDRJ'4IDATc?doi g0gvaqπh%0CIENDB`PNG  IHDR6c;1IDAT јD(p<] DIҥSENVnTh8IENDB`PNG  IHDR66&LVIDATx^KN1`'wE p_pNK"FVjMD]iڔpPۉ{=vNq_|FAL99P0檱$Y`j.V3TP#P81]5Xzv6明nX4V"{nb4w35׭qFd ~W鱇Pl {{Nӝyy"T`xH ,\&6xOuww#cc2ڕ"-NQ8 P0~|Z?H"U`U Hk&[|0UZ \#[XMytFQBJd6[&,9b$rL9:TaBLPe+̒ "TMC$ j{<C(OF'w)B$e$9$ \*9J)9ϒǔAϔK9,%H*O湌T*OrI'ɜ8/fT !F4IENDB`PNG  IHDR66&LVIDATx^AN@ `@r d*+/שf5Y=OStI/ڴˌ8C#֎ۇU{ppۿŎk-,v["54_0?@T8')L1К$[Dɰ0/Ŕo5`g n p)2ek SS=uc![{U^/V0mȱ/U7KGq2-u^.!xDGh@;Q93 Auʢ9F1'flV)imJ$F3]6Ws;CBIwA6R7So"~$9A:ECsH㛄 ,W:%ǍW,'&"L.oQ)GDlU,BDb,9eTc;SimMHYAoT|"hL[dӴ)`1M1N&řH͹Rď'"RɊei[IENDB`PNG  IHDR9dR8|IDATx^kj1 '"NMhO[%zƚ!3)vlkߎ@zDE| u Uϭ Uu\ l;N% ,}78cv«[yS¡!s&T^kN~vƖ.iib^h 'xN_}9%3)ϭ8JBTxg`WJ -)#)VV=0s;lm|l%g7\ PzL@4Lԁ]m=Α x4>/F+n3"OuI+|RL1y%o.2MJu2;_!7gIENDB`PNG  IHDR9dR8IDATx^mN@QC4+t soLN)w̴Ng d+c}$")6h.Lwq3N3nzlFM768d\mKIgWN.25.=k *;sm,BEkqV⾓sI]w:־?GC+{ |bp_ta|#/ NƤb lR=0f~6$Pp5o<}o ڬGR[p`w42M{\۹.6's]LIENDB`PNG  IHDR9%>IDATx^ˊ1D?4fADDDf,@򞍗#ia٥S9G=Xe:Vfnd #Hnm)_OW[Od|s*5tlK\*IwQR 5$a4VyiVUW1FЗۂ):#`c(ߵq#\WZ;OTỗV O#/fWk؆''/^7y`"Ho*ۊ;@X]I0j PpӔEOsۂ+|X)b fUz#M3ΨYE?vB iXrIENDB`PNG  IHDRIDATc`Al g IENDB`PNG  IHDRdqWIDATx^!0ip$MPH%8E:euNҷda笑cb$Gó#xD_@ Sr?Ь/FIENDB`PNG  IHDRdqXIDATx^1@0ѯMۄ  FC#&&/&m%<}] R2fgc!2@Oץ3-%H@Sy| IENDB`PNG  IHDR8EaIDATc?1؀ؕ .cIENDB`PNG  IHDR8EaIDATc`\ `.IENDB`PNG  IHDRIDATc?X0x!̤IENDB`PNG  IHDRdq?IDATx^! )fwD`K0B3 }^`uELN^ 2ˣgg,IENDB`PNG  IHDRdq@IDAT(c@T0% `h ] 1h2(WRd_ !%N ,0bSQe!V'IENDB`PNG  IHDR '>NIDATx^A `Ec".(MpMt˨ǃK p胧v67ܬB+3.VKr #= c0,8QSy1Tdr&ȑ _ *F*\p T8u'K R;@sIENDB`PNG  IHDR إ?dIDATx^ѱ@@E;$++DؖHtD?F&yYj3=z^.0)HvЬYAB =#qS96U`C\:V;aIENDB`PNG  IHDR إ?gIDATx^! @FŠx *M1شEaə>& ,40H r@$0$LA%p@ $ `WNaVJ\:8IENDB`PNG  IHDR '>N@IDATx^10PVM/S!XkjPa苊ʹQӂ&L`Q{ @:# IENDB` Accessibility

Accessibility

Global accessibility mode:
/* * Copyright (c) 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ body { font-family: Arial, sans-serif; font-size: 12px; margin: 10px; min-width: 47em; padding-bottom: 65px; } img { float: left; height: 16px; padding-right: 5px; width: 16px; } .row { border-bottom: 1px solid #A0A0A0; padding: 5px; } .url { color: #A0A0A0; } // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('accessibility', function() { 'use strict'; function requestData() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'targets-data.json', false); xhr.send(null); if (xhr.status === 200) { console.log(xhr.responseText); return JSON.parse(xhr.responseText); } return []; } // TODO(aboxhall): add a mechanism to request individual and global a11y // mode, xhr them on toggle... or just re-requestData and be smarter about // ID-ing rows? function toggleAccessibility(data, element) { chrome.send('toggleAccessibility', [String(data.processId), String(data.routeId)]); var a11y_was_on = (element.textContent.match(/on/) != null); element.textContent = ' accessibility ' + (a11y_was_on ? ' off' : ' on'); var row = element.parentElement; if (a11y_was_on) { while (row.lastChild != element) row.removeChild(row.lastChild); } else { row.appendChild(document.createTextNode(' | ')); row.appendChild(createShowAccessibilityTreeElement(data, row, false)); } } function requestAccessibilityTree(data, element) { chrome.send('requestAccessibilityTree', [String(data.processId), String(data.routeId)]); } function toggleGlobalAccessibility() { chrome.send('toggleGlobalAccessibility'); document.location.reload(); // FIXME see TODO above } function initialize() { console.log('initialize'); var data = requestData(); addGlobalAccessibilityModeToggle(data['global_a11y_mode']); $('pages').textContent = ''; var list = data['list']; for (var i = 0; i < list.length; i++) { addToPagesList(list[i]); } } function addGlobalAccessibilityModeToggle(global_a11y_mode) { $('toggle_global').textContent = (global_a11y_mode == 0 ? 'off' : 'on'); $('toggle_global').addEventListener('click', toggleGlobalAccessibility); } function addToPagesList(data) { // TODO: iterate through data and pages rows instead var id = data['processId'] + '.' + data['routeId']; var row = document.createElement('div'); row.className = 'row'; row.id = id; formatRow(row, data); row.processId = data.processId; row.routeId = data.routeId; var list = $('pages'); list.appendChild(row); } function formatRow(row, data) { if (!('url' in data)) { if ('error' in data) { row.appendChild(createErrorMessageElement(data, row)); return; } } var properties = ['favicon_url', 'name', 'url']; for (var j = 0; j < properties.length; j++) row.appendChild(formatValue(data, properties[j])); row.appendChild(createToggleAccessibilityElement(data)); if (data['a11y_mode'] != 0) { row.appendChild(document.createTextNode(' | ')); if ('tree' in data) { row.appendChild(createShowAccessibilityTreeElement(data, row, true)); row.appendChild(document.createTextNode(' | ')); row.appendChild(createHideAccessibilityTreeElement(row.id)); row.appendChild(createAccessibilityTreeElement(data)); } else { row.appendChild(createShowAccessibilityTreeElement(data, row, false)); if ('error' in data) row.appendChild(createErrorMessageElement(data, row)); } } } function formatValue(data, property) { var value = data[property]; if (property == 'favicon_url') { var faviconElement = document.createElement('img'); if (value) faviconElement.src = value; faviconElement.alt = ""; return faviconElement; } var text = value ? String(value) : ''; if (text.length > 100) text = text.substring(0, 100) + '\u2026'; // ellipsis var span = document.createElement('span'); span.textContent = ' ' + text + ' '; span.className = property; return span; } function createToggleAccessibilityElement(data) { var link = document.createElement('a'); link.setAttribute('href', '#'); var a11y_mode = data['a11y_mode']; link.textContent = 'accessibility ' + (a11y_mode == 0 ? 'off' : 'on'); link.addEventListener('click', toggleAccessibility.bind(this, data, link)); return link; } function createShowAccessibilityTreeElement(data, row, opt_refresh) { var link = document.createElement('a'); link.setAttribute('href', '#'); if (opt_refresh) link.textContent = 'refresh accessibility tree'; else link.textContent = 'show accessibility tree'; link.id = row.id + ':showTree'; link.addEventListener('click', requestAccessibilityTree.bind(this, data, link)); return link; } function createHideAccessibilityTreeElement(id) { var link = document.createElement('a'); link.setAttribute('href', '#'); link.textContent = 'hide accessibility tree'; link.addEventListener('click', function() { $(id + ':showTree').textContent = 'show accessibility tree'; var existingTreeElements = $(id).getElementsByTagName('pre'); for (var i = 0; i < existingTreeElements.length; i++) $(id).removeChild(existingTreeElements[i]); var row = $(id); while (row.lastChild != $(id + ':showTree')) row.removeChild(row.lastChild); }); return link; } function createErrorMessageElement(data) { var errorMessageElement = document.createElement('div'); var errorMessage = data.error; errorMessageElement.innerHTML = errorMessage + ' '; var closeLink = document.createElement('a'); closeLink.href='#'; closeLink.textContent = '[close]'; closeLink.addEventListener('click', function() { var parentElement = errorMessageElement.parentElement; parentElement.removeChild(errorMessageElement); if (parentElement.childElementCount == 0) parentElement.parentElement.removeChild(parentElement); }); errorMessageElement.appendChild(closeLink); return errorMessageElement; } function showTree(data) { var id = data.processId + '.' + data.routeId; var row = $(id); if (!row) return; row.textContent = ''; formatRow(row, data); } function createAccessibilityTreeElement(data) { var treeElement = document.createElement('pre'); var tree = data.tree; treeElement.textContent = tree; return treeElement; } return { initialize: initialize, showTree: showTree }; }); document.addEventListener('DOMContentLoaded', accessibility.initialize);

Graphics Feature Status

Problems Detected

Driver Bug Workarounds

Version Information

Performance Information

Driver Information

Diagnostics

... loading ...
None

Log Messages

  • :
title value
// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('gpu', function() { /** * This class provides a 'bridge' for communicating between javascript and the * browser. When run outside of WebUI, e.g. as a regular webpage, it provides * synthetic data to assist in testing. * @constructor */ function BrowserBridge() { // If we are not running inside WebUI, output chrome.send messages // to the console to help with quick-iteration debugging. this.debugMode_ = (chrome.send === undefined && console.log); if (this.debugMode_) { var browserBridgeTests = document.createElement('script'); browserBridgeTests.src = './gpu_internals/browser_bridge_tests.js'; document.body.appendChild(browserBridgeTests); } this.nextRequestId_ = 0; this.pendingCallbacks_ = []; this.logMessages_ = []; // Tell c++ code that we are ready to receive GPU Info. if (!this.debugMode_) { chrome.send('browserBridgeInitialized'); this.beginRequestClientInfo_(); this.beginRequestLogMessages_(); } } BrowserBridge.prototype = { __proto__: cr.EventTarget.prototype, applySimulatedData_: function applySimulatedData(data) { // set up things according to the simulated data this.gpuInfo_ = data.gpuInfo; this.clientInfo_ = data.clientInfo; this.logMessages_ = data.logMessages; cr.dispatchSimpleEvent(this, 'gpuInfoUpdate'); cr.dispatchSimpleEvent(this, 'clientInfoChange'); cr.dispatchSimpleEvent(this, 'logMessagesChange'); }, /** * Returns true if the page is hosted inside Chrome WebUI * Helps have behavior conditional to emulate_webui.py */ get debugMode() { return this.debugMode_; }, /** * Sends a message to the browser with specified args. The * browser will reply asynchronously via the provided callback. */ callAsync: function(submessage, args, callback) { var requestId = this.nextRequestId_; this.nextRequestId_ += 1; this.pendingCallbacks_[requestId] = callback; if (!args) { chrome.send('callAsync', [requestId.toString(), submessage]); } else { var allArgs = [requestId.toString(), submessage].concat(args); chrome.send('callAsync', allArgs); } }, /** * Called by gpu c++ code when client info is ready. */ onCallAsyncReply: function(requestId, args) { if (this.pendingCallbacks_[requestId] === undefined) { throw new Error('requestId ' + requestId + ' is not pending'); } var callback = this.pendingCallbacks_[requestId]; callback(args); delete this.pendingCallbacks_[requestId]; }, /** * Get gpuInfo data. */ get gpuInfo() { return this.gpuInfo_; }, /** * Called from gpu c++ code when GPU Info is updated. */ onGpuInfoUpdate: function(gpuInfo) { this.gpuInfo_ = gpuInfo; cr.dispatchSimpleEvent(this, 'gpuInfoUpdate'); }, /** * This function begins a request for the ClientInfo. If it comes back * as undefined, then we will issue the request again in 250ms. */ beginRequestClientInfo_: function() { this.callAsync('requestClientInfo', undefined, (function(data) { if (data === undefined) { // try again in 250 ms window.setTimeout(this.beginRequestClientInfo_.bind(this), 250); } else { this.clientInfo_ = data; cr.dispatchSimpleEvent(this, 'clientInfoChange'); } }).bind(this)); }, /** * Returns information about the currently running Chrome build. */ get clientInfo() { return this.clientInfo_; }, /** * This function checks for new GPU_LOG messages. * If any are found, a refresh is triggered. */ beginRequestLogMessages_: function() { this.callAsync('requestLogMessages', undefined, (function(messages) { if (messages.length != this.logMessages_.length) { this.logMessages_ = messages; cr.dispatchSimpleEvent(this, 'logMessagesChange'); } // check again in 250 ms window.setTimeout(this.beginRequestLogMessages_.bind(this), 250); }).bind(this)); }, /** * Returns an array of log messages issued by the GPU process, if any. */ get logMessages() { return this.logMessages_; }, }; return { BrowserBridge: BrowserBridge }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview This view displays information on the current GPU * hardware. Its primary usefulness is to allow users to copy-paste * their data in an easy to read format for bug reports. */ cr.define('gpu', function() { /** * Provides information on the GPU process and underlying graphics hardware. * @constructor * @extends {cr.ui.TabPanel} */ var InfoView = cr.ui.define(cr.ui.TabPanel); InfoView.prototype = { __proto__: cr.ui.TabPanel.prototype, decorate: function() { cr.ui.TabPanel.prototype.decorate.apply(this); browserBridge.addEventListener('gpuInfoUpdate', this.refresh.bind(this)); browserBridge.addEventListener('logMessagesChange', this.refresh.bind(this)); browserBridge.addEventListener('clientInfoChange', this.refresh.bind(this)); this.refresh(); }, /** * Updates the view based on its currently known data */ refresh: function(data) { // Client info if (browserBridge.clientInfo) { var clientInfo = browserBridge.clientInfo; this.setTable_('client-info', [ { description: 'Data exported', value: (new Date()).toLocaleString() }, { description: 'Chrome version', value: clientInfo.version }, { description: 'Operating system', value: clientInfo.operating_system }, { description: 'Software rendering list version', value: clientInfo.blacklist_version }, { description: 'Driver bug list version', value: clientInfo.driver_bug_list_version }, { description: 'ANGLE revision', value: clientInfo.angle_revision }, { description: '2D graphics backend', value: clientInfo.graphics_backend }]); } else { this.setText_('client-info', '... loading...'); } // Feature map var featureLabelMap = { '2d_canvas': 'Canvas', '3d_css': '3D CSS', 'css_animation': 'CSS Animation', 'compositing': 'Compositing', 'webgl': 'WebGL', 'multisampling': 'WebGL multisampling', 'flash_3d': 'Flash 3D', 'flash_stage3d': 'Flash Stage3D', 'flash_stage3d_baseline': 'Flash Stage3D Baseline profile', 'texture_sharing': 'Texture Sharing', 'video_decode': 'Video Decode', 'video': 'Video', // GPU Switching 'gpu_switching': 'GPU Switching', 'panel_fitting': 'Panel Fitting', 'force_compositing_mode': 'Force Compositing Mode', 'raster': 'Rasterization', }; var statusLabelMap = { 'disabled_software': 'Software only. Hardware acceleration disabled.', 'disabled_software_animated': 'Software animated.', 'disabled_off': 'Unavailable. Hardware acceleration disabled.', 'software': 'Software rendered. Hardware acceleration not enabled.', 'unavailable_off': 'Unavailable. Hardware acceleration unavailable', 'unavailable_software': 'Software only, hardware acceleration unavailable', 'enabled_readback': 'Hardware accelerated, but at reduced performance', 'enabled_force': 'Hardware accelerated on all pages', 'enabled_threaded': 'Hardware accelerated on demand and threaded', 'enabled_force_threaded': 'Hardware accelerated on all pages and threaded', 'enabled': 'Hardware accelerated', 'accelerated': 'Accelerated', 'accelerated_threaded': 'Accelerated and threaded', // GPU Switching 'gpu_switching_automatic': 'Automatic switching', 'gpu_switching_force_discrete': 'Always on discrete GPU', 'gpu_switching_force_integrated': 'Always on integrated GPU', 'disabled_software_multithreaded': 'Software only, multi-threaded', }; var statusClassMap = { 'disabled_software': 'feature-yellow', 'disabled_software_animated': 'feature-yellow', 'disabled_off': 'feature-red', 'software': 'feature-yellow', 'unavailable_off': 'feature-red', 'unavailable_software': 'feature-yellow', 'enabled_force': 'feature-green', 'enabled_readback': 'feature-yellow', 'enabled_threaded': 'feature-green', 'enabled_force_threaded': 'feature-green', 'enabled': 'feature-green', 'accelerated': 'feature-green', 'accelerated_threaded': 'feature-green', // GPU Switching 'gpu_switching_automatic': 'feature-green', 'gpu_switching_force_discrete': 'feature-red', 'gpu_switching_force_integrated': 'feature-red', 'disabled_software_multithreaded': 'feature-yellow', }; // GPU info, basic var diagnosticsDiv = this.querySelector('.diagnostics'); var diagnosticsLoadingDiv = this.querySelector('.diagnostics-loading'); var featureStatusList = this.querySelector('.feature-status-list'); var problemsDiv = this.querySelector('.problems-div'); var problemsList = this.querySelector('.problems-list'); var workaroundsDiv = this.querySelector('.workarounds-div'); var workaroundsList = this.querySelector('.workarounds-list'); var performanceDiv = this.querySelector('.performance-div'); var gpuInfo = browserBridge.gpuInfo; var i; if (gpuInfo) { // Not using jstemplate here for blacklist status because we construct // href from data, which jstemplate can't seem to do. if (gpuInfo.featureStatus) { // feature status list featureStatusList.textContent = ''; for (i = 0; i < gpuInfo.featureStatus.featureStatus.length; i++) { var feature = gpuInfo.featureStatus.featureStatus[i]; var featureEl = document.createElement('li'); var nameEl = document.createElement('span'); if (!featureLabelMap[feature.name]) console.log('Missing featureLabel for', feature.name); nameEl.textContent = featureLabelMap[feature.name] + ': '; featureEl.appendChild(nameEl); var statusEl = document.createElement('span'); if (!statusLabelMap[feature.status]) console.log('Missing statusLabel for', feature.status); if (!statusClassMap[feature.status]) console.log('Missing statusClass for', feature.status); statusEl.textContent = statusLabelMap[feature.status]; statusEl.className = statusClassMap[feature.status]; featureEl.appendChild(statusEl); featureStatusList.appendChild(featureEl); } // problems list if (gpuInfo.featureStatus.problems.length) { problemsDiv.hidden = false; problemsList.textContent = ''; for (i = 0; i < gpuInfo.featureStatus.problems.length; i++) { var problem = gpuInfo.featureStatus.problems[i]; var problemEl = this.createProblemEl_(problem); problemsList.appendChild(problemEl); } } else { problemsDiv.hidden = true; } // driver bug workarounds list if (gpuInfo.featureStatus.workarounds.length) { workaroundsDiv.hidden = false; workaroundsList.textContent = ''; for (i = 0; i < gpuInfo.featureStatus.workarounds.length; i++) { var workaroundEl = document.createElement('li'); workaroundEl.textContent = gpuInfo.featureStatus.workarounds[i]; workaroundsList.appendChild(workaroundEl); } } else { workaroundsDiv.hidden = true; } } else { featureStatusList.textContent = ''; problemsList.hidden = true; workaroundsList.hidden = true; } if (gpuInfo.basic_info) this.setTable_('basic-info', gpuInfo.basic_info); else this.setTable_('basic-info', []); if (gpuInfo.performance_info) { performanceDiv.hidden = false; this.setTable_('performance-info', gpuInfo.performance_info); } else { performanceDiv.hidden = true; } if (gpuInfo.diagnostics) { diagnosticsDiv.hidden = false; diagnosticsLoadingDiv.hidden = true; $('diagnostics-table').hidden = false; this.setTable_('diagnostics-table', gpuInfo.diagnostics); } else if (gpuInfo.diagnostics === null) { // gpu_internals.cc sets diagnostics to null when it is being loaded diagnosticsDiv.hidden = false; diagnosticsLoadingDiv.hidden = false; $('diagnostics-table').hidden = true; } else { diagnosticsDiv.hidden = true; } } else { this.setText_('basic-info', '... loading ...'); diagnosticsDiv.hidden = true; featureStatusList.textContent = ''; problemsDiv.hidden = true; } // Log messages jstProcess(new JsEvalContext({values: browserBridge.logMessages}), $('log-messages')); }, createProblemEl_: function(problem) { var problemEl; problemEl = document.createElement('li'); // Description of issue var desc = document.createElement('a'); desc.textContent = problem.description; problemEl.appendChild(desc); // Spacing ':' element if (problem.crBugs.length + problem.webkitBugs.length > 0) { var tmp = document.createElement('span'); tmp.textContent = ': '; problemEl.appendChild(tmp); } var nbugs = 0; var j; // crBugs for (j = 0; j < problem.crBugs.length; ++j) { if (nbugs > 0) { var tmp = document.createElement('span'); tmp.textContent = ', '; problemEl.appendChild(tmp); } var link = document.createElement('a'); var bugid = parseInt(problem.crBugs[j]); link.textContent = bugid; link.href = 'http://crbug.com/' + bugid; problemEl.appendChild(link); nbugs++; } for (j = 0; j < problem.webkitBugs.length; ++j) { if (nbugs > 0) { var tmp = document.createElement('span'); tmp.textContent = ', '; problemEl.appendChild(tmp); } var link = document.createElement('a'); var bugid = parseInt(problem.webkitBugs[j]); link.textContent = bugid; link.href = 'https://bugs.webkit.org/show_bug.cgi?id=' + bugid; problemEl.appendChild(link); nbugs++; } return problemEl; }, setText_: function(outputElementId, text) { var peg = document.getElementById(outputElementId); peg.textContent = text; }, setTable_: function(outputElementId, inputData) { var template = jstGetTemplate('info-view-table-template'); jstProcess(new JsEvalContext({value: inputData}), template); var peg = document.getElementById(outputElementId); if (!peg) throw new Error('Node ' + outputElementId + ' not found'); peg.innerHTML = ''; peg.appendChild(template); } }; return { InfoView: InfoView }; }); var browserBridge; /** * Main entry point. called once the page has loaded. */ function onLoad() { browserBridge = new gpu.BrowserBridge(); // Create the views. cr.ui.decorate('#info-view', gpu.InfoView); } document.addEventListener('DOMContentLoaded', onLoad); IndexedDB
Instances in:
Size:
Last modified:
Open connections:
Path:
Force close Download
Open database:
Connections: open: pending opens: pending upgrades: running upgrades: pending deletes:
Transactions:
Process ID ID Mode Scope Completed Requests Pending Requests Age (ms) Runtime (ms) Status

IndexedDB

// Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('indexeddb', function() { 'use strict'; function initialize() { chrome.send('getAllOrigins'); } function progressNodeFor(link) { return link.parentNode.querySelector('.download-status'); } function downloadOriginData(event) { var link = event.target; progressNodeFor(link).style.display = 'inline'; chrome.send('downloadOriginData', [link.idb_partition_path, link.idb_origin_url]); return false; } function forceClose(event) { var link = event.target; progressNodeFor(link).style.display = 'inline'; chrome.send('forceClose', [link.idb_partition_path, link.idb_origin_url]); return false; } function withNode(selector, partition_path, origin_url, callback) { var links = document.querySelectorAll(selector); for (var i = 0; i < links.length; ++i) { var link = links[i]; if (partition_path == link.idb_partition_path && origin_url == link.idb_origin_url) { callback(link); } } } // Fired from the backend after the data has been zipped up, and the // download manager has begun downloading the file. function onOriginDownloadReady(partition_path, origin_url, connection_count) { withNode('a.download', partition_path, origin_url, function(link) { progressNodeFor(link).style.display = 'none'; }); withNode('.connection-count', partition_path, origin_url, function(span) { span.innerText = connection_count; }); } function onForcedClose(partition_path, origin_url, connection_count) { withNode('a.force-close', partition_path, origin_url, function(link) { progressNodeFor(link).style.display = 'none'; }); withNode('.connection-count', partition_path, origin_url, function(span) { span.innerText = connection_count; }); } // Fired from the backend with a single partition's worth of // IndexedDB metadata. function onOriginsReady(origins, partition_path) { var template = jstGetTemplate('indexeddb-list-template'); var container = $('indexeddb-list'); container.appendChild(template); jstProcess(new JsEvalContext({ idbs: origins, partition_path: partition_path}), template); var downloadLinks = container.querySelectorAll('a.download'); for (var i = 0; i < downloadLinks.length; ++i) { downloadLinks[i].addEventListener('click', downloadOriginData, false); } var forceCloseLinks = container.querySelectorAll('a.force-close'); for (i = 0; i < forceCloseLinks.length; ++i) { forceCloseLinks[i].addEventListener('click', forceClose, false); } } return { initialize: initialize, onForcedClose: onForcedClose, onOriginDownloadReady: onOriginDownloadReady, onOriginsReady: onOriginsReady, }; }); document.addEventListener('DOMContentLoaded', indexeddb.initialize); /* Copyright (c) 2013 The Chromium Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ .indexeddb-summary { background-color: rgb(235, 239, 249); border-top: 1px solid rgb(156, 194, 239); margin-bottom: 6px; margin-top: 12px; padding: 3px; font-weight: bold; } .indexeddb-item { margin-bottom: 15px; margin-top: 6px; position: relative; } .indexeddb-url { color: rgb(85, 102, 221); display: inline-block; max-width: 500px; overflow: hidden; padding-bottom: 1px; padding-top: 4px; text-decoration: none; text-overflow: ellipsis; white-space: nowrap; } .indexeddb-database { margin-bottom: 6px; margin-top: 6px; margin-left: 12px; position: relative; } .indexeddb-database > div { margin-left: 12px; } .indexeddb-connection-count { margin: 0 8px; } .indexeddb-connection-count.pending { font-weight: bold; } .indexeddb-transaction-list { margin-left: 10px; border-collapse: collapse; } .indexeddb-transaction-list th, .indexeddb-transaction-list td { padding: 2px 10px; min-width: 50px; max-width: 75px; } td.indexeddb-transaction-scope { min-width: 200px; max-width: 500px; } .indexeddb-transaction-list th { background-color: rgb(249, 249, 249); border: 1px solid rgb(156, 194, 239); font-weight: normal; text-align: left; } .indexeddb-transaction { background-color: rgb(235, 239, 249); border-bottom: 2px solid white; } .indexeddb-transaction.created { font-weight: italic; } .indexeddb-transaction.started { font-weight: bold; } .indexeddb-transaction.running { font-weight: bold; } .indexeddb-transaction.blocked { } .indexeddb-transaction.started .indexeddb-transaction-state { background-color: rgb(249, 249, 235); } .indexeddb-transaction.running .indexeddb-transaction-state { background-color: rgb(235, 249, 235); } .indexeddb-transaction.blocked .indexeddb-transaction-state { background-color: rgb(249, 235, 235); } .controls a { -webkit-margin-end: 16px; color: #777; }

Players

    Audio Streams

      Properties

      Property Name Value

        Log

        Timestamp Property Value
        // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var media = {}; // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * A global object that gets used by the C++ interface. */ var media = (function() { 'use strict'; var manager = null; // A number->string mapping that is populated through the backend that // describes the phase that the network entity is in. var eventPhases = {}; // A number->string mapping that is populated through the backend that // describes the type of event sent from the network. var eventTypes = {}; // A mapping of number->CacheEntry where the number is a unique id for that // network request. var cacheEntries = {}; // A mapping of url->CacheEntity where the url is the url of the resource. var cacheEntriesByKey = {}; var requrestURLs = {}; var media = { BAR_WIDTH: 200, BAR_HEIGHT: 25 }; /** * Users of |media| must call initialize prior to calling other methods. */ media.initialize = function(theManager) { manager = theManager; }; media.onReceiveEverything = function(everything) { for (var key in everything.audio_streams) { media.updateAudioStream(everything.audio_streams[key]); } }; media.onReceiveConstants = function(constants) { for (var key in constants.eventTypes) { var value = constants.eventTypes[key]; eventTypes[value] = key; } for (var key in constants.eventPhases) { var value = constants.eventPhases[key]; eventPhases[value] = key; } }; media.cacheForUrl = function(url) { return cacheEntriesByKey[url]; }; media.onNetUpdate = function(updates) { updates.forEach(function(update) { var id = update.source.id; if (!cacheEntries[id]) cacheEntries[id] = new media.CacheEntry; switch (eventPhases[update.phase] + '.' + eventTypes[update.type]) { case 'PHASE_BEGIN.DISK_CACHE_ENTRY_IMPL': var key = update.params.key; // Merge this source with anything we already know about this key. if (cacheEntriesByKey[key]) { cacheEntriesByKey[key].merge(cacheEntries[id]); cacheEntries[id] = cacheEntriesByKey[key]; } else { cacheEntriesByKey[key] = cacheEntries[id]; } cacheEntriesByKey[key].key = key; break; case 'PHASE_BEGIN.SPARSE_READ': cacheEntries[id].readBytes(update.params.offset, update.params.buff_len); cacheEntries[id].sparse = true; break; case 'PHASE_BEGIN.SPARSE_WRITE': cacheEntries[id].writeBytes(update.params.offset, update.params.buff_len); cacheEntries[id].sparse = true; break; case 'PHASE_BEGIN.URL_REQUEST_START_JOB': requrestURLs[update.source.id] = update.params.url; break; case 'PHASE_NONE.HTTP_TRANSACTION_READ_RESPONSE_HEADERS': // Record the total size of the file if this was a range request. var range = /content-range:\s*bytes\s*\d+-\d+\/(\d+)/i.exec( update.params.headers); var key = requrestURLs[update.source.id]; delete requrestURLs[update.source.id]; if (range && key) { if (!cacheEntriesByKey[key]) { cacheEntriesByKey[key] = new media.CacheEntry; cacheEntriesByKey[key].key = key; } cacheEntriesByKey[key].size = range[1]; } break; } }); }; media.onRendererTerminated = function(renderId) { util.object.forEach(manager.players_, function(playerInfo, id) { if (playerInfo.properties['render_id'] == renderId) { manager.removePlayer(id); } }); }; // For whatever reason, addAudioStream is also called on // the removal of audio streams. media.addAudioStream = function(event) { switch (event.status) { case 'created': manager.addAudioStream(event.id); manager.updateAudioStream(event.id, { 'playing': event.playing }); break; case 'closed': manager.removeAudioStream(event.id); break; } }; media.updateAudioStream = function(stream) { manager.addAudioStream(stream.id); manager.updateAudioStream(stream.id, stream); }; media.onItemDeleted = function() { // This only gets called when an audio stream is removed, which // for whatever reason is also handled by addAudioStream... // Because it is already handled, we can safely ignore it. }; media.onPlayerOpen = function(id, timestamp) { manager.addPlayer(id, timestamp); }; media.onMediaEvent = function(event) { var source = event.renderer + ':' + event.player; // Although this gets called on every event, there is nothing we can do // because there is no onOpen event. media.onPlayerOpen(source); manager.updatePlayerInfoNoRecord( source, event.ticksMillis, 'render_id', event.renderer); manager.updatePlayerInfoNoRecord( source, event.ticksMillis, 'player_id', event.player); var propertyCount = 0; util.object.forEach(event.params, function(value, key) { key = key.trim(); // These keys get spammed *a lot*, so put them on the display // but don't log list. if (key === 'buffer_start' || key === 'buffer_end' || key === 'buffer_current' || key === 'is_downloading_data') { manager.updatePlayerInfoNoRecord( source, event.ticksMillis, key, value); } else { manager.updatePlayerInfo(source, event.ticksMillis, key, value); } propertyCount += 1; }); if (propertyCount === 0) { manager.updatePlayerInfo( source, event.ticksMillis, 'EVENT', event.type); } }; // |chrome| is not defined during tests. if (window.chrome && window.chrome.send) { chrome.send('getEverything'); } return media; }()); // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Some utility functions that don't belong anywhere else in the * code. */ var util = (function() { var util = {}; util.object = {}; /** * Calls a function for each element in an object/map/hash. * * @param obj The object to iterate over. * @param f The function to call on every value in the object. F should have * the following arguments: f(value, key, object) where value is the value * of the property, key is the corresponding key, and obj is the object that * was passed in originally. * @param optObj The object use as 'this' within f. */ util.object.forEach = function(obj, f, optObj) { 'use strict'; var key; for (key in obj) { if (obj.hasOwnProperty(key)) { f.call(optObj, obj[key], key, obj); } } }; util.millisecondsToString = function(timeMillis) { function pad(num) { num = num.toString(); if (num.length < 2) { return '0' + num; } return num; } var date = new Date(timeMillis); return pad(date.getUTCHours()) + ':' + pad(date.getUTCMinutes()) + ':' + pad(date.getUTCSeconds()) + ' ' + pad((date.getMilliseconds()) % 1000); }; return util; }()); // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('media', function() { 'use strict'; /** * This class represents a file cached by net. */ function CacheEntry() { this.read_ = new media.DisjointRangeSet; this.written_ = new media.DisjointRangeSet; this.available_ = new media.DisjointRangeSet; // Set to true when we know the entry is sparse. this.sparse = false; this.key = null; this.size = null; // The
        element representing this CacheEntry. this.details_ = document.createElement('details'); this.details_.className = 'cache-entry'; this.details_.open = false; // The
        summary line. It contains a chart of requested file ranges // and the url if we know it. var summary = document.createElement('summary'); this.summaryText_ = document.createTextNode(''); summary.appendChild(this.summaryText_); summary.appendChild(document.createTextNode(' ')); // Controls to modify this CacheEntry. var controls = document.createElement('span'); controls.className = 'cache-entry-controls'; summary.appendChild(controls); summary.appendChild(document.createElement('br')); // A link to clear recorded data from this CacheEntry. var clearControl = document.createElement('a'); clearControl.href = 'javascript:void(0)'; clearControl.onclick = this.clear.bind(this); clearControl.textContent = '(clear entry)'; controls.appendChild(clearControl); this.details_.appendChild(summary); // The canvas for drawing cache writes. this.writeCanvas = document.createElement('canvas'); this.writeCanvas.width = media.BAR_WIDTH; this.writeCanvas.height = media.BAR_HEIGHT; this.details_.appendChild(this.writeCanvas); // The canvas for drawing cache reads. this.readCanvas = document.createElement('canvas'); this.readCanvas.width = media.BAR_WIDTH; this.readCanvas.height = media.BAR_HEIGHT; this.details_.appendChild(this.readCanvas); // A tabular representation of the data in the above canvas. this.detailTable_ = document.createElement('table'); this.detailTable_.className = 'cache-table'; this.details_.appendChild(this.detailTable_); } CacheEntry.prototype = { /** * Mark a range of bytes as read from the cache. * @param {int} start The first byte read. * @param {int} length The number of bytes read. */ readBytes: function(start, length) { start = parseInt(start); length = parseInt(length); this.read_.add(start, start + length); this.available_.add(start, start + length); this.sparse = true; }, /** * Mark a range of bytes as written to the cache. * @param {int} start The first byte written. * @param {int} length The number of bytes written. */ writeBytes: function(start, length) { start = parseInt(start); length = parseInt(length); this.written_.add(start, start + length); this.available_.add(start, start + length); this.sparse = true; }, /** * Merge this CacheEntry with another, merging recorded ranges and flags. * @param {CacheEntry} other The CacheEntry to merge into this one. */ merge: function(other) { this.read_.merge(other.read_); this.written_.merge(other.written_); this.available_.merge(other.available_); this.sparse = this.sparse || other.sparse; this.key = this.key || other.key; this.size = this.size || other.size; }, /** * Clear all recorded ranges from this CacheEntry and redraw this.details_. */ clear: function() { this.read_ = new media.DisjointRangeSet; this.written_ = new media.DisjointRangeSet; this.available_ = new media.DisjointRangeSet; this.generateDetails(); }, /** * Helper for drawCacheReadsToCanvas() and drawCacheWritesToCanvas(). * * Accepts the entries to draw, a canvas fill style, and the canvas to * draw on. */ drawCacheEntriesToCanvas: function(entries, fillStyle, canvas) { // Don't bother drawing anything if we don't know the total size. if (!this.size) { return; } var width = canvas.width; var height = canvas.height; var context = canvas.getContext('2d'); var fileSize = this.size; context.fillStyle = '#aaa'; context.fillRect(0, 0, width, height); function drawRange(start, end) { var left = start / fileSize * width; var right = end / fileSize * width; context.fillRect(left, 0, right - left, height); } context.fillStyle = fillStyle; entries.map(function(start, end) { drawRange(start, end); }); }, /** * Draw cache writes to the given canvas. * * It should consist of a horizontal bar with highlighted sections to * represent which parts of a file have been written to the cache. * * e.g. |xxxxxx----------x| */ drawCacheWritesToCanvas: function(canvas) { this.drawCacheEntriesToCanvas(this.written_, '#00a', canvas); }, /** * Draw cache reads to the given canvas. * * It should consist of a horizontal bar with highlighted sections to * represent which parts of a file have been read from the cache. * * e.g. |xxxxxx----------x| */ drawCacheReadsToCanvas: function(canvas) { this.drawCacheEntriesToCanvas(this.read_, '#0a0', canvas); }, /** * Update this.details_ to contain everything we currently know about * this file. */ generateDetails: function() { function makeElement(tag, content) { var toReturn = document.createElement(tag); toReturn.textContent = content; return toReturn; } this.details_.id = this.key; this.summaryText_.textContent = this.key || 'Unknown File'; this.detailTable_.textContent = ''; var header = document.createElement('thead'); var footer = document.createElement('tfoot'); var body = document.createElement('tbody'); this.detailTable_.appendChild(header); this.detailTable_.appendChild(footer); this.detailTable_.appendChild(body); var headerRow = document.createElement('tr'); headerRow.appendChild(makeElement('th', 'Read From Cache')); headerRow.appendChild(makeElement('th', 'Written To Cache')); header.appendChild(headerRow); var footerRow = document.createElement('tr'); var footerCell = document.createElement('td'); footerCell.textContent = 'Out of ' + (this.size || 'unkown size'); footerCell.setAttribute('colspan', 2); footerRow.appendChild(footerCell); footer.appendChild(footerRow); var read = this.read_.map(function(start, end) { return start + ' - ' + end; }); var written = this.written_.map(function(start, end) { return start + ' - ' + end; }); var length = Math.max(read.length, written.length); for (var i = 0; i < length; i++) { var row = document.createElement('tr'); row.appendChild(makeElement('td', read[i] || '')); row.appendChild(makeElement('td', written[i] || '')); body.appendChild(row); } this.drawCacheWritesToCanvas(this.writeCanvas); this.drawCacheReadsToCanvas(this.readCanvas); }, /** * Render this CacheEntry as a
      • . * @return {HTMLElement} A
      • representing this CacheEntry. */ toListItem: function() { this.generateDetails(); var result = document.createElement('li'); result.appendChild(this.details_); return result; } }; return { CacheEntry: CacheEntry }; }); // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. cr.define('media', function() { /** * This class represents a collection of non-intersecting ranges. Ranges * specified by (start, end) can be added and removed at will. It is used to * record which sections of a media file have been cached, e.g. the first and * last few kB plus several MB in the middle. * * Example usage: * someRange.add(0, 100); // Contains 0-100. * someRange.add(150, 200); // Contains 0-100, 150-200. * someRange.remove(25, 75); // Contains 0-24, 76-100, 150-200. * someRange.add(25, 149); // Contains 0-200. */ function DisjointRangeSet() { this.ranges_ = {}; } DisjointRangeSet.prototype = { /** * Deletes all ranges intersecting with (start ... end) and returns the * extents of the cleared area. * @param {int} start The start of the range to remove. * @param {int} end The end of the range to remove. * @param {int} sloppiness 0 removes only strictly overlapping ranges, and * 1 removes adjacent ones. * @return {Object} The start and end of the newly cleared range. */ clearRange: function(start, end, sloppiness) { var ranges = this.ranges_; var result = {start: start, end: end}; for (var rangeStart in this.ranges_) { rangeEnd = this.ranges_[rangeStart]; // A range intersects another if its start lies within the other range // or vice versa. if ((rangeStart >= start && rangeStart <= (end + sloppiness)) || (start >= rangeStart && start <= (rangeEnd + sloppiness))) { delete ranges[rangeStart]; result.start = Math.min(result.start, rangeStart); result.end = Math.max(result.end, rangeEnd); } } return result; }, /** * Adds a range to this DisjointRangeSet. * Joins adjacent and overlapping ranges together. * @param {int} start The beginning of the range to add, inclusive. * @param {int} end The end of the range to add, inclusive. */ add: function(start, end) { if (end < start) return; // Remove all touching ranges. result = this.clearRange(start, end, 1); // Add back a single contiguous range. this.ranges_[Math.min(start, result.start)] = Math.max(end, result.end); }, /** * Combines a DisjointRangeSet with this one. * @param {DisjointRangeSet} ranges A DisjointRangeSet to be squished into * this one. */ merge: function(other) { var ranges = this; other.forEach(function(start, end) { ranges.add(start, end); }); }, /** * Removes a range from this DisjointRangeSet. * Will split existing ranges if necessary. * @param {int} start The beginning of the range to remove, inclusive. * @param {int} end The end of the range to remove, inclusive. */ remove: function(start, end) { if (end < start) return; // Remove instersecting ranges. result = this.clearRange(start, end, 0); // Add back non-overlapping ranges. if (result.start < start) this.ranges_[result.start] = start - 1; if (result.end > end) this.ranges_[end + 1] = result.end; }, /** * Iterates over every contiguous range in this DisjointRangeSet, calling a * function for each (start, end). * @param {function(int, int)} iterator The function to call on each range. */ forEach: function(iterator) { for (var start in this.ranges_) iterator(start, this.ranges_[start]); }, /** * Maps this DisjointRangeSet to an array by calling a given function on the * start and end of each contiguous range, sorted by start. * @param {function(int, int)} mapper Maps a range to an array element. * @return {Array} An array of each mapper(range). */ map: function(mapper) { var starts = []; for (var start in this.ranges_) starts.push(parseInt(start)); starts.sort(function(a, b) { return a - b; }); var ranges = this.ranges_; var results = starts.map(function(s) { return mapper(s, ranges[s]); }); return results; }, /** * Finds the maximum value present in any of the contained ranges. * @return {int} The maximum value contained by this DisjointRangeSet. */ max: function() { var max = -Infinity; for (var start in this.ranges_) max = Math.max(max, this.ranges_[start]); return max; }, }; return { DisjointRangeSet: DisjointRangeSet }; }); // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview A class for keeping track of the details of a player. */ var PlayerInfo = (function() { 'use strict'; /** * A class that keeps track of properties on a media player. * @param id A unique id that can be used to identify this player. */ function PlayerInfo(id) { this.id = id; // The current value of the properties for this player. this.properties = {}; // All of the past (and present) values of the properties. this.pastValues = {}; // Every single event in the order in which they were received. this.allEvents = []; this.lastRendered = 0; this.firstTimestamp_ = -1; } PlayerInfo.prototype = { /** * Adds or set a property on this player. * This is the default logging method as it keeps track of old values. * @param timestamp The time in milliseconds since the Epoch. * @param key A String key that describes the property. * @param value The value of the property. */ addProperty: function(timestamp, key, value) { // The first timestamp that we get will be recorded. // Then, all future timestamps are deltas of that. if (this.firstTimestamp_ === -1) { this.firstTimestamp_ = timestamp; } if (typeof key !== 'string') { throw new Error(typeof key + ' is not a valid key type'); } this.properties[key] = value; if (!this.pastValues[key]) { this.pastValues[key] = []; } var recordValue = { time: timestamp - this.firstTimestamp_, key: key, value: value }; this.pastValues[key].push(recordValue); this.allEvents.push(recordValue); }, /** * Adds or set a property on this player. * Does not keep track of old values. This is better for * values that get spammed repeatedly. * @param timestamp The time in milliseconds since the Epoch. * @param key A String key that describes the property. * @param value The value of the property. */ addPropertyNoRecord: function(timestamp, key, value) { this.addProperty(timestamp, key, value); this.allEvents.pop(); } }; return PlayerInfo; }()); // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview Keeps track of all the existing PlayerInfo and * audio stream objects and is the entry-point for messages from the backend. * * The events captured by Manager (add, remove, update) are relayed * to the clientRenderer which it can choose to use to modify the UI. */ var Manager = (function() { 'use strict'; function Manager(clientRenderer) { this.players_ = {}; this.audioStreams_ = {}; this.clientRenderer_ = clientRenderer; } Manager.prototype = { /** * Adds an audio-stream to the dictionary of audio-streams to manage. * @param id The unique-id of the audio-stream. */ addAudioStream: function(id) { this.audioStreams_[id] = this.audioStreams_[id] || {}; this.clientRenderer_.audioStreamAdded(this.audioStreams_, this.audioStreams_[id]); }, /** * Sets properties of an audiostream. * @param id The unique-id of the audio-stream. * @param properties A dictionary of properties to be added to the * audio-stream. */ updateAudioStream: function(id, properties) { for (var key in properties) { this.audioStreams_[id][key] = properties[key]; } this.clientRenderer_.audioStreamAdded( this.audioStreams_, this.audioStreams_[id]); }, /** * Removes an audio-stream from the manager. * @param id The unique-id of the audio-stream. */ removeAudioStream: function(id) { this.clientRenderer_.audioStreamRemoved( this.audioStreams_, this.audioStreams_[id]); delete this.audioStreams_[id]; }, /** * Adds a player to the list of players to manage. */ addPlayer: function(id) { if (this.players_[id]) { return; } // Make the PlayerProperty and add it to the mapping this.players_[id] = new PlayerInfo(id); this.clientRenderer_.playerAdded(this.players_, this.players_[id]); }, /** * Attempts to remove a player from the UI. * @param id The ID of the player to remove. */ removePlayer: function(id) { delete this.players_[id]; this.clientRenderer_.playerRemoved(this.players_, this.players_[id]); }, updatePlayerInfoNoRecord: function(id, timestamp, key, value) { if (!this.players_[id]) { console.error('[updatePlayerInfo] Id ' + id + ' does not exist'); return; } this.players_[id].addPropertyNoRecord(timestamp, key, value); this.clientRenderer_.playerUpdated(this.players_, this.players_[id], key, value); }, /** * * @param id The unique ID that identifies the player to be updated. * @param timestamp The timestamp of when the change occured. This * timestamp is *not* normalized. * @param key The name of the property to be added/changed. * @param value The value of the property. */ updatePlayerInfo: function(id, timestamp, key, value) { if (!this.players_[id]) { console.error('[updatePlayerInfo] Id ' + id + ' does not exist'); return; } this.players_[id].addProperty(timestamp, key, value); this.clientRenderer_.playerUpdated(this.players_, this.players_[id], key, value); } }; return Manager; }()); // Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var ClientRenderer = (function() { var ClientRenderer = function() { this.playerListElement = document.getElementById('player-list'); this.audioStreamListElement = document.getElementById('audio-stream-list'); this.propertiesTable = document.getElementById('property-table'); this.logTable = document.getElementById('log'); this.graphElement = document.getElementById('graphs'); this.selectedPlayer = null; this.selectedStream = null; this.selectedPlayerLogIndex = 0; this.filterFunction = function() { return true; }; this.filterText = document.getElementById('filter-text'); this.filterText.onkeyup = this.onTextChange_.bind(this); this.bufferCanvas = document.createElement('canvas'); this.bufferCanvas.width = media.BAR_WIDTH; this.bufferCanvas.height = media.BAR_HEIGHT; this.clipboardTextarea = document.getElementById('clipboard-textarea'); this.clipboardButton = document.getElementById('copy-button'); this.clipboardButton.onclick = this.copyToClipboard_.bind(this); }; function removeChildren(element) { while (element.hasChildNodes()) { element.removeChild(element.lastChild); } }; function createButton(text, select_cb) { var button = document.createElement('button'); button.appendChild(document.createTextNode(text)); button.onclick = function() { select_cb(); }; return button; }; ClientRenderer.prototype = { audioStreamAdded: function(audioStreams, audioStreamAdded) { this.redrawAudioStreamList_(audioStreams); }, audioStreamUpdated: function(audioStreams, stream, key, value) { if (stream === this.selectedStream) { this.drawProperties_(stream); } }, audioStreamRemoved: function(audioStreams, audioStreamRemoved) { this.redrawAudioStreamList_(audioStreams); }, /** * Called when a player is added to the collection. * @param players The entire map of id -> player. * @param player_added The player that is added. */ playerAdded: function(players, playerAdded) { this.redrawPlayerList_(players); }, /** * Called when a playre is removed from the collection. * @param players The entire map of id -> player. * @param player_added The player that was removed. */ playerRemoved: function(players, playerRemoved) { this.redrawPlayerList_(players); }, /** * Called when a property on a player is changed. * @param players The entire map of id -> player. * @param player The player that had its property changed. * @param key The name of the property that was changed. * @param value The new value of the property. */ playerUpdated: function(players, player, key, value) { if (player === this.selectedPlayer) { this.drawProperties_(player.properties); this.drawLog_(); this.drawGraphs_(); } if (key === 'name' || key === 'url') { this.redrawPlayerList_(players); } }, redrawAudioStreamList_: function(streams) { removeChildren(this.audioStreamListElement); for (id in streams) { var li = document.createElement('li'); li.appendChild(createButton( id, this.selectAudioStream_.bind(this, streams[id]))); this.audioStreamListElement.appendChild(li); } }, selectAudioStream_: function(audioStream) { this.selectedStream = audioStream; this.selectedPlayer = null; this.drawProperties_(audioStream); removeChildren(this.logTable.querySelector('tbody')); removeChildren(this.graphElement); }, redrawPlayerList_: function(players) { removeChildren(this.playerListElement); for (id in players) { var li = document.createElement('li'); var player = players[id]; var usableName = player.properties.name || player.properties.url || 'player ' + player.id; li.appendChild(createButton( usableName, this.selectPlayer_.bind(this, player))); this.playerListElement.appendChild(li); } }, selectPlayer_: function(player) { this.selectedPlayer = player; this.selectedPlayerLogIndex = 0; this.selectedStream = null; this.drawProperties_(player.properties); removeChildren(this.logTable.querySelector('tbody')); removeChildren(this.graphElement); this.drawLog_(); this.drawGraphs_(); }, drawProperties_: function(propertyMap) { removeChildren(this.propertiesTable); for (key in propertyMap) { var value = propertyMap[key]; var row = this.propertiesTable.insertRow(-1); var keyCell = row.insertCell(-1); var valueCell = row.insertCell(-1); keyCell.appendChild(document.createTextNode(key)); valueCell.appendChild(document.createTextNode(value)); } }, appendEventToLog_: function(event) { if (this.filterFunction(event.key)) { var row = this.logTable.querySelector('tbody').insertRow(-1); row.insertCell(-1).appendChild(document.createTextNode( util.millisecondsToString(event.time))); row.insertCell(-1).appendChild(document.createTextNode(event.key)); row.insertCell(-1).appendChild(document.createTextNode(event.value)); } }, drawLog_: function() { var toDraw = this.selectedPlayer.allEvents.slice( this.selectedPlayerLogIndex); toDraw.forEach(this.appendEventToLog_.bind(this)); this.selectedPlayerLogIndex = this.selectedPlayer.allEvents.length; }, drawGraphs_: function() { function addToGraphs(name, graph, graphElement) { var li = document.createElement('li'); li.appendChild(graph); li.appendChild(document.createTextNode(name)); graphElement.appendChild(li); } var url = this.selectedPlayer.properties.url; if (!url) { return; } var cache = media.cacheForUrl(url); var player = this.selectedPlayer; var props = player.properties; var cacheExists = false; var bufferExists = false; if (props['buffer_start'] !== undefined && props['buffer_current'] !== undefined && props['buffer_end'] !== undefined && props['total_bytes'] !== undefined) { this.drawBufferGraph_(props['buffer_start'], props['buffer_current'], props['buffer_end'], props['total_bytes']); bufferExists = true; } if (cache) { if (player.properties['total_bytes']) { cache.size = Number(player.properties['total_bytes']); } cache.generateDetails(); cacheExists = true; } if (!this.graphElement.hasChildNodes()) { if (bufferExists) { addToGraphs('buffer', this.bufferCanvas, this.graphElement); } if (cacheExists) { addToGraphs('cache read', cache.readCanvas, this.graphElement); addToGraphs('cache write', cache.writeCanvas, this.graphElement); } } }, drawBufferGraph_: function(start, current, end, size) { var ctx = this.bufferCanvas.getContext('2d'); var width = this.bufferCanvas.width; var height = this.bufferCanvas.height; ctx.fillStyle = '#aaa'; ctx.fillRect(0, 0, width, height); var scale_factor = width / size; var left = start * scale_factor; var middle = current * scale_factor; var right = end * scale_factor; ctx.fillStyle = '#a0a'; ctx.fillRect(left, 0, middle - left, height); ctx.fillStyle = '#aa0'; ctx.fillRect(middle, 0, right - middle, height); }, copyToClipboard_: function() { var properties = this.selectedStream || this.selectedPlayer.properties || false; if (!properties) { return; } var stringBuffer = []; for (var key in properties) { var value = properties[key]; stringBuffer.push(key.toString()); stringBuffer.push(': '); stringBuffer.push(value.toString()); stringBuffer.push('\n'); } this.clipboardTextarea.value = stringBuffer.join(''); this.clipboardTextarea.classList.remove('hidden'); this.clipboardTextarea.focus(); this.clipboardTextarea.select(); // The act of copying anything from the textarea gets canceled // if the element in question gets the class 'hidden' (which contains the // css property display:none) before the event is finished. For this, it // is necessary put the property setting on the event loop to be executed // after the copy has taken place. this.clipboardTextarea.oncopy = function(event) { setTimeout(function(element) { event.target.classList.add('hidden'); }, 0); }; }, onTextChange_: function(event) { var text = this.filterText.value.toLowerCase(); var parts = text.split(',').map(function(part) { return part.trim(); }).filter(function(part) { return part.trim().length > 0; }); this.filterFunction = function(text) { text = text.toLowerCase(); return parts.length === 0 || parts.some(function(part) { return text.indexOf(part) != -1; }); }; if (this.selectedPlayer) { removeChildren(this.logTable.querySelector('tbody')); this.selectedPlayerLogIndex = 0; this.drawLog_(); } }, }; return ClientRenderer; })(); media.initialize(new Manager(new ClientRenderer())); WebRTC Internals

        WebRTC Internals

        // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var peerConnectionsListElem = null; var ssrcInfoManager = null; var peerConnectionUpdateTable = null; var statsTable = null; var dumpCreator = null; /** A map from peer connection id to the PeerConnectionRecord. */ var peerConnectionDataStore = {}; /** A simple class to store the updates and stats data for a peer connection. */ var PeerConnectionRecord = (function() { /** @constructor */ function PeerConnectionRecord() { /** @private */ this.record_ = { constraints: {}, servers: [], stats: {}, updateLog: [], url: '', }; }; PeerConnectionRecord.prototype = { /** @override */ toJSON: function() { return this.record_; }, /** * Adds the initilization info of the peer connection. * @param {string} url The URL of the web page owning the peer connection. * @param {Array} servers STUN servers used by the peer connection. * @param {!Object} constraints Media constraints. */ initialize: function(url, servers, constraints) { this.record_.url = url; this.record_.servers = servers; this.record_.constraints = constraints; }, /** * @param {string} dataSeriesId The TimelineDataSeries identifier. * @return {!TimelineDataSeries} */ getDataSeries: function(dataSeriesId) { return this.record_.stats[dataSeriesId]; }, /** * @param {string} dataSeriesId The TimelineDataSeries identifier. * @param {!TimelineDataSeries} dataSeries The TimelineDataSeries to set to. */ setDataSeries: function(dataSeriesId, dataSeries) { this.record_.stats[dataSeriesId] = dataSeries; }, /** * @param {string} type The type of the update. * @param {string} value The value of the update. */ addUpdate: function(type, value) { this.record_.updateLog.push({ time: (new Date()).toLocaleString(), type: type, value: value, }); }, }; return PeerConnectionRecord; })(); // The maximum number of data points bufferred for each stats. Old data points // will be shifted out when the buffer is full. var MAX_STATS_DATA_POINT_BUFFER_SIZE = 1000; // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * A TimelineDataSeries collects an ordered series of (time, value) pairs, * and converts them to graph points. It also keeps track of its color and * current visibility state. * It keeps MAX_STATS_DATA_POINT_BUFFER_SIZE data points at most. Old data * points will be dropped when it reaches this size. */ var TimelineDataSeries = (function() { 'use strict'; /** * @constructor */ function TimelineDataSeries() { // List of DataPoints in chronological order. this.dataPoints_ = []; // Default color. Should always be overridden prior to display. this.color_ = 'red'; // Whether or not the data series should be drawn. this.isVisible_ = true; this.cacheStartTime_ = null; this.cacheStepSize_ = 0; this.cacheValues_ = []; } TimelineDataSeries.prototype = { /** * @override */ toJSON: function() { if (this.dataPoints_.length < 1) return {}; var values = []; for (var i = 0; i < this.dataPoints_.length; ++i) { values.push(this.dataPoints_[i].value); } return { startTime: this.dataPoints_[0].time, endTime: this.dataPoints_[this.dataPoints_.length - 1].time, values: JSON.stringify(values), }; }, /** * Adds a DataPoint to |this| with the specified time and value. * DataPoints are assumed to be received in chronological order. */ addPoint: function(timeTicks, value) { var time = new Date(timeTicks); this.dataPoints_.push(new DataPoint(time, value)); if (this.dataPoints_.length > MAX_STATS_DATA_POINT_BUFFER_SIZE) this.dataPoints_.shift(); }, isVisible: function() { return this.isVisible_; }, show: function(isVisible) { this.isVisible_ = isVisible; }, getColor: function() { return this.color_; }, setColor: function(color) { this.color_ = color; }, /** * Returns a list containing the values of the data series at |count| * points, starting at |startTime|, and |stepSize| milliseconds apart. * Caches values, so showing/hiding individual data series is fast. */ getValues: function(startTime, stepSize, count) { // Use cached values, if we can. if (this.cacheStartTime_ == startTime && this.cacheStepSize_ == stepSize && this.cacheValues_.length == count) { return this.cacheValues_; } // Do all the work. this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count); this.cacheStartTime_ = startTime; this.cacheStepSize_ = stepSize; return this.cacheValues_; }, /** * Returns the cached |values| in the specified time period. */ getValuesInternal_: function(startTime, stepSize, count) { var values = []; var nextPoint = 0; var currentValue = 0; var time = startTime; for (var i = 0; i < count; ++i) { while (nextPoint < this.dataPoints_.length && this.dataPoints_[nextPoint].time < time) { currentValue = this.dataPoints_[nextPoint].value; ++nextPoint; } values[i] = currentValue; time += stepSize; } return values; } }; /** * A single point in a data series. Each point has a time, in the form of * milliseconds since the Unix epoch, and a numeric value. * @constructor */ function DataPoint(time, value) { this.time = time; this.value = value; } return TimelineDataSeries; })(); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Get the ssrc if |report| is an ssrc report. * * @param {!Object} report The object contains id, type, and stats, where stats * is the object containing timestamp and values, which is an array of * strings, whose even index entry is the name of the stat, and the odd * index entry is the value. * @return {?string} The ssrc. */ function GetSsrcFromReport(report) { if (report.type != 'ssrc') { console.warn("Trying to get ssrc from non-ssrc report."); return null; } // If the 'ssrc' name-value pair exists, return the value; otherwise, return // the report id. // The 'ssrc' name-value pair only exists in an upcoming Libjingle change. Old // versions use id to refer to the ssrc. // // TODO(jiayl): remove the fallback to id once the Libjingle change is rolled // to Chrome. if (report.stats && report.stats.values) { for (var i = 0; i < report.stats.values.length - 1; i += 2) { if (report.stats.values[i] == 'ssrc') { return report.stats.values[i + 1]; } } } return report.id; }; /** * SsrcInfoManager stores the ssrc stream info extracted from SDP. */ var SsrcInfoManager = (function() { 'use strict'; /** * @constructor */ function SsrcInfoManager() { /** * Map from ssrc id to an object containing all the stream properties. * @type {!Object.>} * @private */ this.streamInfoContainer_ = {}; /** * The string separating attibutes in an SDP. * @type {string} * @const * @private */ this.ATTRIBUTE_SEPARATOR_ = /[\r,\n]/; /** * The regex separating fields within an ssrc description. * @type {RegExp} * @const * @private */ this.FIELD_SEPARATOR_REGEX_ = / .*:/; /** * The prefix string of an ssrc description. * @type {string} * @const * @private */ this.SSRC_ATTRIBUTE_PREFIX_ = 'a=ssrc:'; /** * The className of the ssrc info parent element. * @type {string} * @const */ this.SSRC_INFO_BLOCK_CLASS = 'ssrc-info-block'; } SsrcInfoManager.prototype = { /** * Extracts the stream information from |sdp| and saves it. * For example: * a=ssrc:1234 msid:abcd * a=ssrc:1234 label:hello * * @param {string} sdp The SDP string. */ addSsrcStreamInfo: function(sdp) { var attributes = sdp.split(this.ATTRIBUTE_SEPARATOR_); for (var i = 0; i < attributes.length; ++i) { // Check if this is a ssrc attribute. if (attributes[i].indexOf(this.SSRC_ATTRIBUTE_PREFIX_) != 0) continue; var nextFieldIndex = attributes[i].search(this.FIELD_SEPARATOR_REGEX_); if (nextFieldIndex == -1) continue; var ssrc = attributes[i].substring(this.SSRC_ATTRIBUTE_PREFIX_.length, nextFieldIndex); if (!this.streamInfoContainer_[ssrc]) this.streamInfoContainer_[ssrc] = {}; // Make |rest| starting at the next field. var rest = attributes[i].substring(nextFieldIndex + 1); var name, value; while (rest.length > 0) { nextFieldIndex = rest.search(this.FIELD_SEPARATOR_REGEX_); if (nextFieldIndex == -1) nextFieldIndex = rest.length; // The field name is the string before the colon. name = rest.substring(0, rest.indexOf(':')); // The field value is from after the colon to the next field. value = rest.substring(rest.indexOf(':') + 1, nextFieldIndex); this.streamInfoContainer_[ssrc][name] = value; // Move |rest| to the start of the next field. rest = rest.substring(nextFieldIndex + 1); } } }, /** * @param {string} sdp The ssrc id. * @return {!Object.} The object containing the ssrc infomation. */ getStreamInfo: function(ssrc) { return this.streamInfoContainer_[ssrc]; }, /** * Populate the ssrc information into |parentElement|, each field as a * DIV element. * * @param {!Element} parentElement The parent element for the ssrc info. * @param {string} ssrc The ssrc id. */ populateSsrcInfo: function(parentElement, ssrc) { if (!this.streamInfoContainer_[ssrc]) return; parentElement.className = this.SSRC_INFO_BLOCK_CLASS; var fieldElement; for (var property in this.streamInfoContainer_[ssrc]) { fieldElement = document.createElement('div'); parentElement.appendChild(fieldElement); fieldElement.textContent = property + ':' + this.streamInfoContainer_[ssrc][property]; } } }; return SsrcInfoManager; })(); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // // This file contains helper methods to draw the stats timeline graphs. // Each graph represents a series of stats report for a PeerConnection, // e.g. 1234-0-ssrc-abcd123-bytesSent is the graph for the series of bytesSent // for ssrc-abcd123 of PeerConnection 0 in process 1234. // The graphs are drawn as CANVAS, grouped per report type per PeerConnection. // Each group has an expand/collapse button and is collapsed initially. // // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * A TimelineGraphView displays a timeline graph on a canvas element. */ var TimelineGraphView = (function() { 'use strict'; // Default starting scale factor, in terms of milliseconds per pixel. var DEFAULT_SCALE = 1000; // Maximum number of labels placed vertically along the sides of the graph. var MAX_VERTICAL_LABELS = 6; // Vertical spacing between labels and between the graph and labels. var LABEL_VERTICAL_SPACING = 4; // Horizontal spacing between vertically placed labels and the edges of the // graph. var LABEL_HORIZONTAL_SPACING = 3; // Horizintal spacing between two horitonally placed labels along the bottom // of the graph. var LABEL_LABEL_HORIZONTAL_SPACING = 25; // Length of ticks, in pixels, next to y-axis labels. The x-axis only has // one set of labels, so it can use lines instead. var Y_AXIS_TICK_LENGTH = 10; var GRID_COLOR = '#CCC'; var TEXT_COLOR = '#000'; var BACKGROUND_COLOR = '#FFF'; /** * @constructor */ function TimelineGraphView(divId, canvasId) { this.scrollbar_ = {position_: 0, range_: 0}; this.graphDiv_ = $(divId); this.canvas_ = $(canvasId); // Set the range and scale of the graph. Times are in milliseconds since // the Unix epoch. // All measurements we have must be after this time. this.startTime_ = 0; // The current rightmost position of the graph is always at most this. this.endTime_ = 1; this.graph_ = null; // Initialize the scrollbar. this.updateScrollbarRange_(true); } TimelineGraphView.prototype = { // Returns the total length of the graph, in pixels. getLength_: function() { var timeRange = this.endTime_ - this.startTime_; // Math.floor is used to ignore the last partial area, of length less // than DEFAULT_SCALE. return Math.floor(timeRange / DEFAULT_SCALE); }, /** * Returns true if the graph is scrolled all the way to the right. */ graphScrolledToRightEdge_: function() { return this.scrollbar_.position_ == this.scrollbar_.range_; }, /** * Update the range of the scrollbar. If |resetPosition| is true, also * sets the slider to point at the rightmost position and triggers a * repaint. */ updateScrollbarRange_: function(resetPosition) { var scrollbarRange = this.getLength_() - this.canvas_.width; if (scrollbarRange < 0) scrollbarRange = 0; // If we've decreased the range to less than the current scroll position, // we need to move the scroll position. if (this.scrollbar_.position_ > scrollbarRange) resetPosition = true; this.scrollbar_.range_ = scrollbarRange; if (resetPosition) { this.scrollbar_.position_ = scrollbarRange; this.repaint(); } }, /** * Sets the date range displayed on the graph, switches to the default * scale factor, and moves the scrollbar all the way to the right. */ setDateRange: function(startDate, endDate) { this.startTime_ = startDate.getTime(); this.endTime_ = endDate.getTime(); // Safety check. if (this.endTime_ <= this.startTime_) this.startTime_ = this.endTime_ - 1; this.updateScrollbarRange_(true); }, /** * Updates the end time at the right of the graph to be the current time. * Specifically, updates the scrollbar's range, and if the scrollbar is * all the way to the right, keeps it all the way to the right. Otherwise, * leaves the view as-is and doesn't redraw anything. */ updateEndDate: function() { this.endTime_ = (new Date()).getTime(); this.updateScrollbarRange_(this.graphScrolledToRightEdge_()); }, getStartDate: function() { return new Date(this.startTime_); }, /** * Replaces the current TimelineDataSeries with |dataSeries|. */ setDataSeries: function(dataSeries) { // Simply recreates the Graph. this.graph_ = new Graph(); for (var i = 0; i < dataSeries.length; ++i) this.graph_.addDataSeries(dataSeries[i]); this.repaint(); }, /** * Adds |dataSeries| to the current graph. */ addDataSeries: function(dataSeries) { if (!this.graph_) this.graph_ = new Graph(); this.graph_.addDataSeries(dataSeries); this.repaint(); }, /** * Draws the graph on |canvas_|. */ repaint: function() { this.repaintTimerRunning_ = false; var width = this.canvas_.width; var height = this.canvas_.height; var context = this.canvas_.getContext('2d'); // Clear the canvas. context.fillStyle = BACKGROUND_COLOR; context.fillRect(0, 0, width, height); // Try to get font height in pixels. Needed for layout. var fontHeightString = context.font.match(/([0-9]+)px/)[1]; var fontHeight = parseInt(fontHeightString); // Safety check, to avoid drawing anything too ugly. if (fontHeightString.length == 0 || fontHeight <= 0 || fontHeight * 4 > height || width < 50) { return; } // Save current transformation matrix so we can restore it later. context.save(); // The center of an HTML canvas pixel is technically at (0.5, 0.5). This // makes near straight lines look bad, due to anti-aliasing. This // translation reduces the problem a little. context.translate(0.5, 0.5); // Figure out what time values to display. var position = this.scrollbar_.position_; // If the entire time range is being displayed, align the right edge of // the graph to the end of the time range. if (this.scrollbar_.range_ == 0) position = this.getLength_() - this.canvas_.width; var visibleStartTime = this.startTime_ + position * DEFAULT_SCALE; // Make space at the bottom of the graph for the time labels, and then // draw the labels. var textHeight = height; height -= fontHeight + LABEL_VERTICAL_SPACING; this.drawTimeLabels(context, width, height, textHeight, visibleStartTime); // Draw outline of the main graph area. context.strokeStyle = GRID_COLOR; context.strokeRect(0, 0, width - 1, height - 1); if (this.graph_) { // Layout graph and have them draw their tick marks. this.graph_.layout( width, height, fontHeight, visibleStartTime, DEFAULT_SCALE); this.graph_.drawTicks(context); // Draw the lines of all graphs, and then draw their labels. this.graph_.drawLines(context); this.graph_.drawLabels(context); } // Restore original transformation matrix. context.restore(); }, /** * Draw time labels below the graph. Takes in start time as an argument * since it may not be |startTime_|, when we're displaying the entire * time range. */ drawTimeLabels: function(context, width, height, textHeight, startTime) { // Draw the labels 1 minute apart. var timeStep = 1000 * 60; // Find the time for the first label. This time is a perfect multiple of // timeStep because of how UTC times work. var time = Math.ceil(startTime / timeStep) * timeStep; context.textBaseline = 'bottom'; context.textAlign = 'center'; context.fillStyle = TEXT_COLOR; context.strokeStyle = GRID_COLOR; // Draw labels and vertical grid lines. while (true) { var x = Math.round((time - startTime) / DEFAULT_SCALE); if (x >= width) break; var text = (new Date(time)).toLocaleTimeString(); context.fillText(text, x, textHeight); context.beginPath(); context.lineTo(x, 0); context.lineTo(x, height); context.stroke(); time += timeStep; } }, getDataSeriesCount: function() { if (this.graph_) return this.graph_.dataSeries_.length; return 0; }, hasDataSeries: function(dataSeries) { if (this.graph_) return this.graph_.hasDataSeries(dataSeries); return false; }, }; /** * A Graph is responsible for drawing all the TimelineDataSeries that have * the same data type. Graphs are responsible for scaling the values, laying * out labels, and drawing both labels and lines for its data series. */ var Graph = (function() { /** * @constructor */ function Graph() { this.dataSeries_ = []; // Cached properties of the graph, set in layout. this.width_ = 0; this.height_ = 0; this.fontHeight_ = 0; this.startTime_ = 0; this.scale_ = 0; // At least the highest value in the displayed range of the graph. // Used for scaling and setting labels. Set in layoutLabels. this.max_ = 0; // Cached text of equally spaced labels. Set in layoutLabels. this.labels_ = []; } /** * A Label is the label at a particular position along the y-axis. * @constructor */ function Label(height, text) { this.height = height; this.text = text; } Graph.prototype = { addDataSeries: function(dataSeries) { this.dataSeries_.push(dataSeries); }, hasDataSeries: function(dataSeries) { for (var i = 0; i < this.dataSeries_.length; ++i) { if (this.dataSeries_[i] == dataSeries) return true; } return false; }, /** * Returns a list of all the values that should be displayed for a given * data series, using the current graph layout. */ getValues: function(dataSeries) { if (!dataSeries.isVisible()) return null; return dataSeries.getValues(this.startTime_, this.scale_, this.width_); }, /** * Updates the graph's layout. In particular, both the max value and * label positions are updated. Must be called before calling any of the * drawing functions. */ layout: function(width, height, fontHeight, startTime, scale) { this.width_ = width; this.height_ = height; this.fontHeight_ = fontHeight; this.startTime_ = startTime; this.scale_ = scale; // Find largest value. var max = 0; for (var i = 0; i < this.dataSeries_.length; ++i) { var values = this.getValues(this.dataSeries_[i]); if (!values) continue; for (var j = 0; j < values.length; ++j) { if (values[j] > max) max = values[j]; } } this.layoutLabels_(max); }, /** * Lays out labels and sets |max_|, taking the time units into * consideration. |maxValue| is the actual maximum value, and * |max_| will be set to the value of the largest label, which * will be at least |maxValue|. */ layoutLabels_: function(maxValue) { if (maxValue < 1024) { this.layoutLabelsBasic_(maxValue, 0); return; } // Find appropriate units to use. var units = ['', 'k', 'M', 'G', 'T', 'P']; // Units to use for labels. 0 is '1', 1 is K, etc. // We start with 1, and work our way up. var unit = 1; maxValue /= 1024; while (units[unit + 1] && maxValue >= 1024) { maxValue /= 1024; ++unit; } // Calculate labels. this.layoutLabelsBasic_(maxValue, 1); // Append units to labels. for (var i = 0; i < this.labels_.length; ++i) this.labels_[i] += ' ' + units[unit]; // Convert |max_| back to unit '1'. this.max_ *= Math.pow(1024, unit); }, /** * Same as layoutLabels_, but ignores units. |maxDecimalDigits| is the * maximum number of decimal digits allowed. The minimum allowed * difference between two adjacent labels is 10^-|maxDecimalDigits|. */ layoutLabelsBasic_: function(maxValue, maxDecimalDigits) { this.labels_ = []; // No labels if |maxValue| is 0. if (maxValue == 0) { this.max_ = maxValue; return; } // The maximum number of equally spaced labels allowed. |fontHeight_| // is doubled because the top two labels are both drawn in the same // gap. var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING; // The + 1 is for the top label. var maxLabels = 1 + this.height_ / minLabelSpacing; if (maxLabels < 2) { maxLabels = 2; } else if (maxLabels > MAX_VERTICAL_LABELS) { maxLabels = MAX_VERTICAL_LABELS; } // Initial try for step size between conecutive labels. var stepSize = Math.pow(10, -maxDecimalDigits); // Number of digits to the right of the decimal of |stepSize|. // Used for formating label strings. var stepSizeDecimalDigits = maxDecimalDigits; // Pick a reasonable step size. while (true) { // If we use a step size of |stepSize| between labels, we'll need: // // Math.ceil(maxValue / stepSize) + 1 // // labels. The + 1 is because we need labels at both at 0 and at // the top of the graph. // Check if we can use steps of size |stepSize|. if (Math.ceil(maxValue / stepSize) + 1 <= maxLabels) break; // Check |stepSize| * 2. if (Math.ceil(maxValue / (stepSize * 2)) + 1 <= maxLabels) { stepSize *= 2; break; } // Check |stepSize| * 5. if (Math.ceil(maxValue / (stepSize * 5)) + 1 <= maxLabels) { stepSize *= 5; break; } stepSize *= 10; if (stepSizeDecimalDigits > 0) --stepSizeDecimalDigits; } // Set the max so it's an exact multiple of the chosen step size. this.max_ = Math.ceil(maxValue / stepSize) * stepSize; // Create labels. for (var label = this.max_; label >= 0; label -= stepSize) this.labels_.push(label.toFixed(stepSizeDecimalDigits)); }, /** * Draws tick marks for each of the labels in |labels_|. */ drawTicks: function(context) { var x1; var x2; x1 = this.width_ - 1; x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH; context.fillStyle = GRID_COLOR; context.beginPath(); for (var i = 1; i < this.labels_.length - 1; ++i) { // The rounding is needed to avoid ugly 2-pixel wide anti-aliased // lines. var y = Math.round(this.height_ * i / (this.labels_.length - 1)); context.moveTo(x1, y); context.lineTo(x2, y); } context.stroke(); }, /** * Draws a graph line for each of the data series. */ drawLines: function(context) { // Factor by which to scale all values to convert them to a number from // 0 to height - 1. var scale = 0; var bottom = this.height_ - 1; if (this.max_) scale = bottom / this.max_; // Draw in reverse order, so earlier data series are drawn on top of // subsequent ones. for (var i = this.dataSeries_.length - 1; i >= 0; --i) { var values = this.getValues(this.dataSeries_[i]); if (!values) continue; context.strokeStyle = this.dataSeries_[i].getColor(); context.beginPath(); for (var x = 0; x < values.length; ++x) { // The rounding is needed to avoid ugly 2-pixel wide anti-aliased // horizontal lines. context.lineTo(x, bottom - Math.round(values[x] * scale)); } context.stroke(); } }, /** * Draw labels in |labels_|. */ drawLabels: function(context) { if (this.labels_.length == 0) return; var x = this.width_ - LABEL_HORIZONTAL_SPACING; // Set up the context. context.fillStyle = TEXT_COLOR; context.textAlign = 'right'; // Draw top label, which is the only one that appears below its tick // mark. context.textBaseline = 'top'; context.fillText(this.labels_[0], x, 0); // Draw all the other labels. context.textBaseline = 'bottom'; var step = (this.height_ - 1) / (this.labels_.length - 1); for (var i = 1; i < this.labels_.length; ++i) context.fillText(this.labels_[i], x, step * i); } }; return Graph; })(); return TimelineGraphView; })(); var STATS_GRAPH_CONTAINER_HEADING_CLASS = 'stats-graph-container-heading'; // Specifies which stats should be drawn on the 'bweCompound' graph and how. var bweCompoundGraphConfig = { googAvailableSendBandwidth: {color: 'red'}, googTargetEncBitrateCorrected: {color: 'purple'}, googActualEncBitrate: {color: 'orange'}, googRetransmitBitrate: {color: 'blue'}, googTransmitBitrate: {color: 'green'}, }; // Converts the last entry of |srcDataSeries| from the total amount to the // amount per second. var totalToPerSecond = function(srcDataSeries) { var length = srcDataSeries.dataPoints_.length; if (length >= 2) { var lastDataPoint = srcDataSeries.dataPoints_[length - 1]; var secondLastDataPoint = srcDataSeries.dataPoints_[length - 2]; return (lastDataPoint.value - secondLastDataPoint.value) * 1000 / (lastDataPoint.time - secondLastDataPoint.time); } return 0; }; // Converts the value of total bytes to bits per second. var totalBytesToBitsPerSecond = function(srcDataSeries) { return totalToPerSecond(srcDataSeries) * 8; }; // Specifies which stats should be converted before drawn and how. // |convertedName| is the name of the converted value, |convertFunction| // is the function used to calculate the new converted value based on the // original dataSeries. var dataConversionConfig = { packetsSent: { convertedName: 'packetsSentPerSecond', convertFunction: totalToPerSecond, }, bytesSent: { convertedName: 'bitsSentPerSecond', convertFunction: totalBytesToBitsPerSecond, }, packetsReceived: { convertedName: 'packetsReceivedPerSecond', convertFunction: totalToPerSecond, }, bytesReceived: { convertedName: 'bitsReceivedPerSecond', convertFunction: totalBytesToBitsPerSecond, }, // This is due to a bug of wrong units reported for googTargetEncBitrate. // TODO (jiayl): remove this when the unit bug is fixed. googTargetEncBitrate: { convertedName: 'googTargetEncBitrateCorrected', convertFunction: function (srcDataSeries) { var length = srcDataSeries.dataPoints_.length; var lastDataPoint = srcDataSeries.dataPoints_[length - 1]; if (lastDataPoint.value < 5000) return lastDataPoint.value * 1000; return lastDataPoint.value; } } }; // The object contains the stats names that should not be added to the graph, // even if they are numbers. var statsNameBlackList = { 'ssrc': true, 'googTrackId': true, 'googComponent': true, 'googLocalAddress': true, 'googRemoteAddress': true, }; var graphViews = {}; // Returns number parsed from |value|, or NaN if the stats name is black-listed. function getNumberFromValue(name, value) { if (statsNameBlackList[name]) return NaN; return parseFloat(value); } // Adds the stats report |report| to the timeline graph for the given // |peerConnectionElement|. function drawSingleReport(peerConnectionElement, report) { var reportType = report.type; var reportId = report.id; var stats = report.stats; if (!stats || !stats.values) return; for (var i = 0; i < stats.values.length - 1; i = i + 2) { var rawLabel = stats.values[i]; var rawDataSeriesId = reportId + '-' + rawLabel; var rawValue = getNumberFromValue(rawLabel, stats.values[i + 1]); if (isNaN(rawValue)) { // We do not draw non-numerical values, but still want to record it in the // data series. addDataSeriesPoint(peerConnectionElement, rawDataSeriesId, stats.timestamp, rawLabel, stats.values[i + 1]); continue; } var finalDataSeriesId = rawDataSeriesId; var finalLabel = rawLabel; var finalValue = rawValue; // We need to convert the value if dataConversionConfig[rawLabel] exists. if (dataConversionConfig[rawLabel]) { // Updates the original dataSeries before the conversion. addDataSeriesPoint(peerConnectionElement, rawDataSeriesId, stats.timestamp, rawLabel, rawValue); // Convert to another value to draw on graph, using the original // dataSeries as input. finalValue = dataConversionConfig[rawLabel].convertFunction( peerConnectionDataStore[peerConnectionElement.id].getDataSeries( rawDataSeriesId)); finalLabel = dataConversionConfig[rawLabel].convertedName; finalDataSeriesId = reportId + '-' + finalLabel; } // Updates the final dataSeries to draw. addDataSeriesPoint(peerConnectionElement, finalDataSeriesId, stats.timestamp, finalLabel, finalValue); // Updates the graph. var graphType = bweCompoundGraphConfig[finalLabel] ? 'bweCompound' : finalLabel; var graphViewId = peerConnectionElement.id + '-' + reportId + '-' + graphType; if (!graphViews[graphViewId]) { graphViews[graphViewId] = createStatsGraphView(peerConnectionElement, report, graphType); var date = new Date(stats.timestamp); graphViews[graphViewId].setDateRange(date, date); } // Adds the new dataSeries to the graphView. We have to do it here to cover // both the simple and compound graph cases. var dataSeries = peerConnectionDataStore[peerConnectionElement.id].getDataSeries( finalDataSeriesId); if (!graphViews[graphViewId].hasDataSeries(dataSeries)) graphViews[graphViewId].addDataSeries(dataSeries); graphViews[graphViewId].updateEndDate(); } } // Makes sure the TimelineDataSeries with id |dataSeriesId| is created, // and adds the new data point to it. function addDataSeriesPoint( peerConnectionElement, dataSeriesId, time, label, value) { var dataSeries = peerConnectionDataStore[peerConnectionElement.id].getDataSeries( dataSeriesId); if (!dataSeries) { dataSeries = new TimelineDataSeries(); peerConnectionDataStore[peerConnectionElement.id].setDataSeries( dataSeriesId, dataSeries); if (bweCompoundGraphConfig[label]) { dataSeries.setColor(bweCompoundGraphConfig[label].color); } } dataSeries.addPoint(time, value); } // Ensures a div container to hold all stats graphs for one track is created as // a child of |peerConnectionElement|. function ensureStatsGraphTopContainer(peerConnectionElement, report) { var containerId = peerConnectionElement.id + '-' + report.type + '-' + report.id + '-graph-container'; var container = $(containerId); if (!container) { container = document.createElement('details'); container.id = containerId; container.className = 'stats-graph-container'; peerConnectionElement.appendChild(container); container.innerHTML =''; container.firstChild.firstChild.className = STATS_GRAPH_CONTAINER_HEADING_CLASS; container.firstChild.firstChild.textContent = 'Stats graphs for ' + report.id; if (report.type == 'ssrc') { var ssrcInfoElement = document.createElement('div'); container.firstChild.appendChild(ssrcInfoElement); ssrcInfoManager.populateSsrcInfo(ssrcInfoElement, GetSsrcFromReport(report)); } } return container; } // Creates the container elements holding a timeline graph // and the TimelineGraphView object. function createStatsGraphView( peerConnectionElement, report, statsName) { var topContainer = ensureStatsGraphTopContainer(peerConnectionElement, report); var graphViewId = peerConnectionElement.id + '-' + report.id + '-' + statsName; var divId = graphViewId + '-div'; var canvasId = graphViewId + '-canvas'; var container = document.createElement("div"); container.className = 'stats-graph-sub-container'; topContainer.appendChild(container); container.innerHTML = '
        ' + statsName + '
        ' + '
        '; if (statsName == 'bweCompound') { container.insertBefore( createBweCompoundLegend(peerConnectionElement, report.id), $(divId)); } return new TimelineGraphView(divId, canvasId); } // Creates the legend section for the bweCompound graph. // Returns the legend element. function createBweCompoundLegend(peerConnectionElement, reportId) { var legend = document.createElement('div'); for (var prop in bweCompoundGraphConfig) { var div = document.createElement('div'); legend.appendChild(div); div.innerHTML = '' + prop; div.style.color = bweCompoundGraphConfig[prop].color; div.dataSeriesId = reportId + '-' + prop; div.graphViewId = peerConnectionElement.id + '-' + reportId + '-bweCompound'; div.firstChild.addEventListener('click', function(event) { var target = peerConnectionDataStore[peerConnectionElement.id].getDataSeries( event.target.parentNode.dataSeriesId); target.show(event.target.checked); graphViews[event.target.parentNode.graphViewId].repaint(); }); } return legend; } // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Maintains the stats table. * @param {SsrcInfoManager} ssrcInfoManager The source of the ssrc info. */ var StatsTable = (function(ssrcInfoManager) { 'use strict'; /** * @param {SsrcInfoManager} ssrcInfoManager The source of the ssrc info. * @constructor */ function StatsTable(ssrcInfoManager) { /** * @type {SsrcInfoManager} * @private */ this.ssrcInfoManager_ = ssrcInfoManager; } StatsTable.prototype = { /** * Adds |report| to the stats table of |peerConnectionElement|. * * @param {!Element} peerConnectionElement The root element. * @param {!Object} report The object containing stats, which is the object * containing timestamp and values, which is an array of strings, whose * even index entry is the name of the stat, and the odd index entry is * the value. */ addStatsReport: function(peerConnectionElement, report) { var statsTable = this.ensureStatsTable_(peerConnectionElement, report); if (report.stats) { this.addStatsToTable_(statsTable, report.stats.timestamp, report.stats.values); } }, /** * Ensure the DIV container for the stats tables is created as a child of * |peerConnectionElement|. * * @param {!Element} peerConnectionElement The root element. * @return {!Element} The stats table container. * @private */ ensureStatsTableContainer_: function(peerConnectionElement) { var containerId = peerConnectionElement.id + '-table-container'; var container = $(containerId); if (!container) { container = document.createElement('div'); container.id = containerId; container.className = 'stats-table-container'; peerConnectionElement.appendChild(container); } return container; }, /** * Ensure the stats table for track specified by |report| of PeerConnection * |peerConnectionElement| is created. * * @param {!Element} peerConnectionElement The root element. * @param {!Object} report The object containing stats, which is the object * containing timestamp and values, which is an array of strings, whose * even index entry is the name of the stat, and the odd index entry is * the value. * @return {!Element} The stats table element. * @private */ ensureStatsTable_: function(peerConnectionElement, report) { var tableId = peerConnectionElement.id + '-table-' + report.id; var table = $(tableId); if (!table) { var container = this.ensureStatsTableContainer_(peerConnectionElement); table = document.createElement('table'); container.appendChild(table); table.id = tableId; table.border = 1; table.innerHTML = ''; table.rows[0].cells[0].textContent = 'Statistics ' + report.id; if (report.type == 'ssrc') { table.insertRow(1); table.rows[1].innerHTML = ''; this.ssrcInfoManager_.populateSsrcInfo( table.rows[1].cells[0], GetSsrcFromReport(report)); } } return table; }, /** * Update |statsTable| with |time| and |statsData|. * * @param {!Element} statsTable Which table to update. * @param {number} time The number of miliseconds since epoch. * @param {Array.} statsData An array of stats name and value pairs. * @private */ addStatsToTable_: function(statsTable, time, statsData) { var date = Date(time); this.updateStatsTableRow_(statsTable, 'timestamp', date.toLocaleString()); for (var i = 0; i < statsData.length - 1; i = i + 2) { this.updateStatsTableRow_(statsTable, statsData[i], statsData[i + 1]); } }, /** * Update the value column of the stats row of |rowName| to |value|. * A new row is created is this is the first report of this stats. * * @param {!Element} statsTable Which table to update. * @param {string} rowName The name of the row to update. * @param {string} value The new value to set. * @private */ updateStatsTableRow_: function(statsTable, rowName, value) { var trId = statsTable.id + '-' + rowName; var trElement = $(trId); if (!trElement) { trElement = document.createElement('tr'); trElement.id = trId; statsTable.firstChild.appendChild(trElement); trElement.innerHTML = '' + rowName + ''; } trElement.cells[1].textContent = value; } }; return StatsTable; })(); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * The data of a peer connection update. * @param {number} pid The id of the renderer. * @param {number} lid The id of the peer conneciton inside a renderer. * @param {string} type The type of the update. * @param {string} value The details of the update. * @constructor */ var PeerConnectionUpdateEntry = function(pid, lid, type, value) { /** * @type {number} */ this.pid = pid; /** * @type {number} */ this.lid = lid; /** * @type {string} */ this.type = type; /** * @type {string} */ this.value = value; }; /** * Maintains the peer connection update log table. */ var PeerConnectionUpdateTable = (function() { 'use strict'; /** * @constructor */ function PeerConnectionUpdateTable() { /** * @type {string} * @const * @private */ this.UPDATE_LOG_ID_SUFFIX_ = '-update-log'; /** * @type {string} * @const * @private */ this.UPDATE_LOG_CONTAINER_CLASS_ = 'update-log-container'; /** * @type {string} * @const * @private */ this.UPDATE_LOG_TABLE_CLASS = 'update-log-table'; } PeerConnectionUpdateTable.prototype = { /** * Adds the update to the update table as a new row. The type of the update * is set to the summary of the cell; clicking the cell will reveal or hide * the details as the content of a TextArea element. * * @param {!Element} peerConnectionElement The root element. * @param {!PeerConnectionUpdateEntry} update The update to add. */ addPeerConnectionUpdate: function(peerConnectionElement, update) { var tableElement = this.ensureUpdateContainer_(peerConnectionElement); var row = document.createElement('tr'); tableElement.firstChild.appendChild(row); row.innerHTML = '' + (new Date()).toLocaleString() + ''; if (update.value.length == 0) { row.innerHTML += '' + update.type + ''; return; } row.innerHTML += '
        ' + update.type + '
        '; var valueContainer = document.createElement('pre'); var details = row.cells[1].childNodes[0]; details.appendChild(valueContainer); valueContainer.textContent = update.value; }, /** * Makes sure the update log table of the peer connection is created. * * @param {!Element} peerConnectionElement The root element. * @return {!Element} The log table element. * @private */ ensureUpdateContainer_: function(peerConnectionElement) { var tableId = peerConnectionElement.id + this.UPDATE_LOG_ID_SUFFIX_; var tableElement = $(tableId); if (!tableElement) { var tableContainer = document.createElement('div'); tableContainer.className = this.UPDATE_LOG_CONTAINER_CLASS_; peerConnectionElement.appendChild(tableContainer); tableElement = document.createElement('table'); tableElement.className = this.UPDATE_LOG_TABLE_CLASS; tableElement.id = tableId; tableElement.border = 1; tableContainer.appendChild(tableElement); tableElement.innerHTML = 'Time' + 'Event'; } return tableElement; } }; return PeerConnectionUpdateTable; })(); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Provides the UI to start and stop RTP recording, forwards the start/stop * commands to Chrome, and updates the UI based on dump updates. Also provides * creating a file containing all PeerConnection updates and stats. */ var DumpCreator = (function() { /** * @param {Element} containerElement The parent element of the dump creation * UI. * @constructor */ function DumpCreator(containerElement) { /** * True if the RTP packets are being recorded. * @type {bool} * @private */ this.recording_ = false; /** * @type {!Object.} * @private * @const */ this.StatusStrings_ = { NOT_STARTED: 'not started.', RECORDING: 'recording...', }, /** * The status of dump creation. * @type {string} * @private */ this.status_ = this.StatusStrings_.NOT_STARTED; /** * The root element of the dump creation UI. * @type {Element} * @private */ this.root_ = document.createElement('details'); this.root_.className = 'peer-connection-dump-root'; containerElement.appendChild(this.root_); var summary = document.createElement('summary'); this.root_.appendChild(summary); summary.textContent = 'Create Dump'; var content = document.createElement('pre'); this.root_.appendChild(content); content.innerHTML = ' Status: ' + '
        '; content.getElementsByTagName('button')[0].addEventListener( 'click', this.onRtpToggled_.bind(this)); content.getElementsByTagName('button')[1].addEventListener( 'click', this.onDownloadData_.bind(this)); this.updateDisplay_(); } DumpCreator.prototype = { /** * Downloads the PeerConnection updates and stats data as a file. * * @private */ onDownloadData_: function() { var textBlob = new Blob([JSON.stringify(peerConnectionDataStore, null, ' ')], {type: 'octet/stream'}); var URL = window.webkitURL.createObjectURL(textBlob); this.root_.getElementsByTagName('form')[0].action = URL; // The default action of the button will submit the form. }, /** * Handles the event of toggling the rtp recording state. * * @private */ onRtpToggled_: function() { if (this.recording_) { this.recording_ = false; this.status_ = this.StatusStrings_.NOT_STARTED; chrome.send('stopRtpRecording'); } else { this.recording_ = true; this.status_ = this.StatusStrings_.RECORDING; chrome.send('startRtpRecording'); } this.updateDisplay_(); }, /** * Updates the UI based on the recording status. * * @private */ updateDisplay_: function() { if (this.recording_) { this.root_.getElementsByTagName('button')[0].textContent = 'Stop Recording RTP Packets'; } else { this.root_.getElementsByTagName('button')[0].textContent = 'Start Recording RTP Packets'; } this.root_.getElementsByTagName('span')[0].textContent = this.status_; }, /** * Set the status to the content of the update. * @param {!Object} update */ onUpdate: function(update) { if (this.recording_) { this.status_ = JSON.stringify(update); this.updateDisplay_(); } }, }; return DumpCreator; })(); function initialize() { peerConnectionsListElem = $('peer-connections-list'); dumpCreator = new DumpCreator(peerConnectionsListElem); ssrcInfoManager = new SsrcInfoManager(); peerConnectionUpdateTable = new PeerConnectionUpdateTable(); statsTable = new StatsTable(ssrcInfoManager); chrome.send('getAllUpdates'); // Requests stats from all peer connections every second. window.setInterval(function() { if (peerConnectionsListElem.getElementsByTagName('li').length > 0) chrome.send('getAllStats'); }, 1000); } document.addEventListener('DOMContentLoaded', initialize); /** * A helper function for getting a peer connection element id. * * @param {!Object.} data The object containing the pid and lid * of the peer connection. * @return {string} The peer connection element id. */ function getPeerConnectionId(data) { return data.pid + '-' + data.lid; } /** * Extracts ssrc info from a setLocal/setRemoteDescription update. * * @param {!PeerConnectionUpdateEntry} data The peer connection update data. */ function extractSsrcInfo(data) { if (data.type == 'setLocalDescription' || data.type == 'setRemoteDescription') { ssrcInfoManager.addSsrcStreamInfo(data.value); } } /** * Helper for adding a peer connection update. * * @param {Element} peerConnectionElement * @param {!PeerConnectionUpdateEntry} update The peer connection update data. */ function addPeerConnectionUpdate(peerConnectionElement, update) { peerConnectionUpdateTable.addPeerConnectionUpdate(peerConnectionElement, update); extractSsrcInfo(update); peerConnectionDataStore[peerConnectionElement.id].addUpdate( update.type, update.value); } /** Browser message handlers. */ /** * Removes all information about a peer connection. * * @param {!Object.} data The object containing the pid and lid * of a peer connection. */ function removePeerConnection(data) { var element = $(getPeerConnectionId(data)); if (element) { delete peerConnectionDataStore[element.id]; peerConnectionsListElem.removeChild(element); } } /** * Adds a peer connection. * * @param {!Object} data The object containing the pid, lid, url, servers, and * constraints of a peer connection. */ function addPeerConnection(data) { var id = getPeerConnectionId(data); if (!peerConnectionDataStore[id]) { peerConnectionDataStore[id] = new PeerConnectionRecord(); } peerConnectionDataStore[id].initialize( data.url, data.servers, data.constraints); var peerConnectionElement = $(id); if (!peerConnectionElement) { peerConnectionElement = document.createElement('li'); peerConnectionsListElem.appendChild(peerConnectionElement); peerConnectionElement.id = id; } peerConnectionElement.innerHTML = '

        PeerConnection ' + peerConnectionElement.id + '

        ' + '
        ' + data.url + ' ' + data.servers + ' ' + data.constraints + '
        '; // Clicking the heading can expand or collapse the peer connection item. peerConnectionElement.firstChild.title = 'Click to collapse or expand'; peerConnectionElement.firstChild.addEventListener('click', function(e) { if (e.target.parentElement.className == '') e.target.parentElement.className = 'peer-connection-hidden'; else e.target.parentElement.className = ''; }); return peerConnectionElement; } /** * Adds a peer connection update. * * @param {!PeerConnectionUpdateEntry} data The peer connection update data. */ function updatePeerConnection(data) { var peerConnectionElement = $(getPeerConnectionId(data)); addPeerConnectionUpdate(peerConnectionElement, data); } /** * Adds the information of all peer connections created so far. * * @param {Array.} data An array of the information of all peer * connections. Each array item contains pid, lid, url, servers, * constraints, and an array of updates as the log. */ function updateAllPeerConnections(data) { for (var i = 0; i < data.length; ++i) { var peerConnection = addPeerConnection(data[i]); var log = data[i].log; if (!log) continue; for (var j = 0; j < log.length; ++j) { addPeerConnectionUpdate(peerConnection, log[j]); } } } /** * Handles the report of stats. * * @param {!Object} data The object containing pid, lid, and reports, where * reports is an array of stats reports. Each report contains id, type, * and stats, where stats is the object containing timestamp and values, * which is an array of strings, whose even index entry is the name of the * stat, and the odd index entry is the value. */ function addStats(data) { var peerConnectionElement = $(getPeerConnectionId(data)); if (!peerConnectionElement) return; for (var i = 0; i < data.reports.length; ++i) { var report = data.reports[i]; statsTable.addStatsReport(peerConnectionElement, report); drawSingleReport(peerConnectionElement, report); } } /** * Delegates to dumpCreator to update the recording status. * @param {!Object.} update Key-value pairs describing the status of the * RTP recording. */ function updateDumpStatus(update) { dumpCreator.onUpdate(update); } chrome://tracing
        // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. * WARNING: This file is generated by generate_about_tracing_contents.py * * Do not edit directly. */ window.FLATTENED = {}; window.FLATTENED_RAW_SCRIPTS = {}; window.FLATTENED['base'] = true; window.FLATTENED['base.event_target'] = true; window.FLATTENED['base.events'] = true; window.FLATTENED['base.properties'] = true; window.FLATTENED['base.iteration_helpers'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/gl-matrix/src/gl-matrix/common.js'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/gl-matrix/src/gl-matrix/mat2d.js'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/gl-matrix/src/gl-matrix/mat4.js'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/gl-matrix/src/gl-matrix/vec2.js'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/gl-matrix/src/gl-matrix/vec3.js'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/gl-matrix/src/gl-matrix/vec4.js'] = true; window.FLATTENED['base.gl_matrix'] = true; window.FLATTENED['base.rect'] = true; window.FLATTENED['base.utils'] = true; window.FLATTENED['ui'] = true; window.FLATTENED['ui.overlay'] = true; window.FLATTENED['about_tracing.tracing_controller'] = true; window.FLATTENED['base.bbox2'] = true; window.FLATTENED['cc.constants'] = true; window.FLATTENED['cc.region'] = true; window.FLATTENED['cc.tile_coverage_rect'] = true; window.FLATTENED['base.range'] = true; window.FLATTENED['base.sorted_array_utils'] = true; window.FLATTENED['base.guid'] = true; window.FLATTENED['tracing.trace_model.event'] = true; window.FLATTENED['tracing.trace_model.object_snapshot'] = true; window.FLATTENED['tracing.trace_model.object_instance'] = true; window.FLATTENED['cc.layer_impl'] = true; window.FLATTENED['cc.layer_tree_impl'] = true; window.FLATTENED['base.quad'] = true; window.FLATTENED['cc.util'] = true; window.FLATTENED['cc.layer_tree_host_impl'] = true; window.FLATTENED['tracing.analysis.util'] = true; window.FLATTENED['base.interval_tree'] = true; window.FLATTENED['tracing.filter'] = true; window.FLATTENED['tracing.importer.importer'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/Promises/polyfill/src/Promise.js'] = true; window.FLATTENED['base.promise'] = true; window.FLATTENED['base.raf'] = true; window.FLATTENED['tracing.importer.task'] = true; window.FLATTENED['tracing.trace_model.counter_sample'] = true; window.FLATTENED['tracing.trace_model.counter_series'] = true; window.FLATTENED['tracing.trace_model.counter'] = true; window.FLATTENED['tracing.trace_model.timed_event'] = true; window.FLATTENED['tracing.trace_model.slice'] = true; window.FLATTENED['tracing.trace_model.cpu'] = true; window.FLATTENED['tracing.trace_model.time_to_object_instance_map'] = true; window.FLATTENED['tracing.trace_model.object_collection'] = true; window.FLATTENED['tracing.trace_model.async_slice'] = true; window.FLATTENED['tracing.trace_model.async_slice_group'] = true; window.FLATTENED['tracing.trace_model.sample'] = true; window.FLATTENED['tracing.color_scheme'] = true; window.FLATTENED['tracing.trace_model.slice_group'] = true; window.FLATTENED['tracing.trace_model.thread'] = true; window.FLATTENED['base.settings'] = true; window.FLATTENED['tracing.trace_model_settings'] = true; window.FLATTENED['tracing.trace_model.process_base'] = true; window.FLATTENED['tracing.trace_model.kernel'] = true; window.FLATTENED['tracing.trace_model.process'] = true; window.FLATTENED['tracing.trace_model'] = true; window.FLATTENED['tracing.trace_model.instant_event'] = true; window.FLATTENED['tracing.selection'] = true; window.FLATTENED['tracing.analysis.analysis_link'] = true; window.FLATTENED['tracing.analysis.generic_object_view'] = true; window.FLATTENED['tracing.analysis.analysis_results'] = true; window.FLATTENED['tracing.analysis.analyze_counters'] = true; window.FLATTENED['tracing.analysis.analyze_slices'] = true; window.FLATTENED['tracing.analysis.analyze_selection'] = true; window.FLATTENED['cc.selection'] = true; window.FLATTENED['ui.dom_helpers'] = true; window.FLATTENED['ui.drag_handle'] = true; window.FLATTENED['ui.container_that_decorates_its_children'] = true; window.FLATTENED['ui.list_view'] = true; window.FLATTENED['cc.layer_picker'] = true; window.FLATTENED['base.color'] = true; window.FLATTENED['cc.debug_colors'] = true; window.FLATTENED['cc.picture_as_image_data'] = true; window.FLATTENED['cc.picture'] = true; window.FLATTENED['cc.tile'] = true; window.FLATTENED['ui.info_bar'] = true; window.FLATTENED['ui.camera'] = true; window.FLATTENED['base.key_event_manager'] = true; window.FLATTENED['tracing.constants'] = true; window.FLATTENED['ui.mouse_tracker'] = true; window.FLATTENED['ui.mouse_mode_selector'] = true; window.FLATTENED['ui.quad_stack_view'] = true; window.FLATTENED['cc.layer_tree_quad_stack_view'] = true; window.FLATTENED['cc.layer_view'] = true; window.FLATTENED['tracing.analysis.object_snapshot_view'] = true; window.FLATTENED['cc.layer_tree_host_impl_view'] = true; window.FLATTENED['cc.picture_ops_chart_summary_view'] = true; window.FLATTENED['cc.picture_ops_chart_view'] = true; window.FLATTENED['cc.picture_ops_list_view'] = true; window.FLATTENED['cc.picture_debugger'] = true; window.FLATTENED['cc.picture_view'] = true; window.FLATTENED['cc.tile_view'] = true; window.FLATTENED['tracing.analysis.slice_view'] = true; window.FLATTENED['cc.raster_task_slice_view'] = true; window.FLATTENED['cc'] = true; window.FLATTENED['gpu.state'] = true; window.FLATTENED['gpu.state_view'] = true; window.FLATTENED['gpu'] = true; window.FLATTENED['tracing.tracks.track'] = true; window.FLATTENED['tracing.tracks.heading_track'] = true; window.FLATTENED['tracing.tracks.object_instance_track'] = true; window.FLATTENED['tracing.tracks.stacked_bars_track'] = true; window.FLATTENED['system_stats.system_stats_instance_track'] = true; window.FLATTENED['system_stats.system_stats_snapshot'] = true; window.FLATTENED['system_stats.system_stats_snapshot_view'] = true; window.FLATTENED['system_stats'] = true; window.FLATTENED['tcmalloc.heap'] = true; window.FLATTENED['tracing.analysis.object_instance_view'] = true; window.FLATTENED['tcmalloc.tcmalloc_instance_view'] = true; window.FLATTENED['tcmalloc.tcmalloc_snapshot_view'] = true; window.FLATTENED['tcmalloc'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/jszip/jszip.js'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/jszip/jszip-inflate.js'] = true; window.FLATTENED['tracing.importer.gzip_importer'] = true; window.FLATTENED['tracing.importer.linux_perf.parser'] = true; window.FLATTENED['tracing.importer.linux_perf.android_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.bus_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.clock_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.cpufreq_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.disk_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.drm_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.exynos_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.gesture_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.i915_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.kfunc_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.mali_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.power_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.sched_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.sync_parser'] = true; window.FLATTENED['tracing.importer.linux_perf.workqueue_parser'] = true; window.FLATTENED['tracing.importer.linux_perf_importer'] = true; window.FLATTENED['tracing.trace_model.flow_event'] = true; window.FLATTENED['tracing.importer.trace_event_importer'] = true; window.FLATTENED['tracing.importer.v8.splaytree'] = true; window.FLATTENED['tracing.importer.v8.codemap'] = true; window.FLATTENED['tracing.importer.v8.log_reader'] = true; window.FLATTENED['tracing.importer.v8_log_importer'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/jszip/jszip.js'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/jszip/jszip-load.js'] = true; window.FLATTENED_RAW_SCRIPTS['../third_party/jszip/jszip-inflate.js'] = true; window.FLATTENED['tracing.importer.zip_importer'] = true; window.FLATTENED['tracing.importer'] = true; window.FLATTENED['tracing.record_selection_dialog'] = true; window.FLATTENED['tracing.analysis.default_object_view'] = true; window.FLATTENED['tracing.analysis.analysis_view'] = true; window.FLATTENED['tracing.analysis.cpu_slice_view'] = true; window.FLATTENED['tracing.analysis.thread_time_slice_view'] = true; window.FLATTENED['ui.animation'] = true; window.FLATTENED['tracing.timeline_display_transform_animations'] = true; window.FLATTENED['tracing.elided_cache'] = true; window.FLATTENED['tracing.draw_helpers'] = true; window.FLATTENED['tracing.timeline_display_transform'] = true; window.FLATTENED['ui.animation_controller'] = true; window.FLATTENED['tracing.timeline_viewport'] = true; window.FLATTENED['tracing.timing_tool'] = true; window.FLATTENED['tracing.tracks.drawing_container'] = true; window.FLATTENED['tracing.tracks.ruler_track'] = true; window.FLATTENED['base.measuring_stick'] = true; window.FLATTENED['tracing.tracks.container_track'] = true; window.FLATTENED['tracing.fast_rect_renderer'] = true; window.FLATTENED['tracing.tracks.slice_track'] = true; window.FLATTENED['tracing.tracks.cpu_track'] = true; window.FLATTENED['tcmalloc.heap_instance_track'] = true; window.FLATTENED['tracing.tracks.counter_track'] = true; window.FLATTENED['tracing.tracks.spacing_track'] = true; window.FLATTENED['tracing.tracks.slice_group_track'] = true; window.FLATTENED['tracing.tracks.async_slice_group_track'] = true; window.FLATTENED['tracing.tracks.thread_track'] = true; window.FLATTENED['tracing.tracks.process_track_base'] = true; window.FLATTENED['tracing.tracks.kernel_track'] = true; window.FLATTENED['tracing.tracks.process_track'] = true; window.FLATTENED['tracing.tracks.trace_model_track'] = true; window.FLATTENED['tracing.timeline_track_view'] = true; window.FLATTENED['tracing.find_control'] = true; window.FLATTENED['tracing.timeline_view'] = true; window.FLATTENED['about_tracing.profiling_view'] = true; // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * The global object. * @type {!Object} * @const */ var global = this; /** Platform, package, object property, and Event support. */ this.base = (function() { /** * Base path for modules. Used to form URLs for module 'require' requests. */ var moduleBasePath = '.'; function setModuleBasePath(path) { if (path[path.length - 1] == '/') path = path.substring(0, path.length - 1); moduleBasePath = path; } function mLog(text, opt_indentLevel) { if (true) return; var spacing = ''; var indentLevel = opt_indentLevel || 0; for (var i = 0; i < indentLevel; i++) spacing += ' '; console.log(spacing + text); } /** * Builds an object structure for the provided namespace path, * ensuring that names that already exist are not overwritten. For * example: * 'a.b.c' -> a = {};a.b={};a.b.c={}; * @param {string} name Name of the object that this file defines. * @param {*=} opt_object The object to expose at the end of the path. * @param {Object=} opt_objectToExportTo The object to add the path to; * default is {@code global}. * @private */ function exportPath(name, opt_object, opt_objectToExportTo) { var parts = name.split('.'); var cur = opt_objectToExportTo || global; for (var part; parts.length && (part = parts.shift());) { if (!parts.length && opt_object !== undefined) { // last part and we have an object; use it cur[part] = opt_object; } else if (part in cur) { cur = cur[part]; } else { cur = cur[part] = {}; } } return cur; }; var didLoadModules = false; var moduleDependencies = {}; var moduleStylesheets = {}; var moduleRawScripts = {}; function addModuleDependency(moduleName, dependentModuleName) { if (!moduleDependencies[moduleName]) moduleDependencies[moduleName] = []; var dependentModules = moduleDependencies[moduleName]; var found = false; for (var i = 0; i < dependentModules.length; i++) if (dependentModules[i] == dependentModuleName) found = true; if (!found) dependentModules.push(dependentModuleName); } function addModuleRawScriptDependency(moduleName, rawScriptName) { if (!moduleRawScripts[moduleName]) moduleRawScripts[moduleName] = []; var dependentRawScripts = moduleRawScripts[moduleName]; var found = false; for (var i = 0; i < moduleRawScripts.length; i++) if (dependentRawScripts[i] == rawScriptName) found = true; if (!found) dependentRawScripts.push(rawScriptName); } function addModuleStylesheet(moduleName, stylesheetName) { if (!moduleStylesheets[moduleName]) moduleStylesheets[moduleName] = []; var stylesheets = moduleStylesheets[moduleName]; var found = false; for (var i = 0; i < stylesheets.length; i++) if (stylesheets[i] == stylesheetName) found = true; if (!found) stylesheets.push(stylesheetName); } function ensureDepsLoaded() { if (window.FLATTENED) return; if (didLoadModules) return; didLoadModules = true; var req = new XMLHttpRequest(); var src = '/deps.js'; req.open('GET', src, false); req.send(null); if (req.status != 200) { var serverSideException = JSON.parse(req.responseText); var msg = 'You have a module problem: ' + serverSideException.message; var baseWarningEl = document.createElement('div'); baseWarningEl.style.backgroundColor = 'white'; baseWarningEl.style.border = '3px solid red'; baseWarningEl.style.boxSizing = 'border-box'; baseWarningEl.style.color = 'black'; baseWarningEl.style.display = '-webkit-flex'; baseWarningEl.style.height = '100%'; baseWarningEl.style.left = 0; baseWarningEl.style.padding = '8px'; baseWarningEl.style.position = 'fixed'; baseWarningEl.style.top = 0; baseWarningEl.style.webkitFlexDirection = 'column'; baseWarningEl.style.width = '100%'; baseWarningEl.innerHTML = '

        Module parsing problem

        ' + '
        ' + '
        ';
              baseWarningEl.querySelector('#message').textContent =
                  serverSideException.message;
              var detailsEl = baseWarningEl.querySelector('#details');
              detailsEl.textContent = serverSideException.details;
              detailsEl.style.webkitFlex = '1 1 auto';
              detailsEl.style.overflow = 'auto';
        
              if (!document.body) {
                setTimeout(function() {
                  document.body.appendChild(baseWarningEl);
                }, 150);
              } else {
                document.body.appendChild(baseWarningEl);
              }
              throw new Error(msg);
            }
        
            base.addModuleDependency = addModuleDependency;
            base.addModuleRawScriptDependency = addModuleRawScriptDependency;
            base.addModuleStylesheet = addModuleStylesheet;
            try {
              // By construction, the deps should call addModuleDependency.
              eval(req.responseText);
            } catch (e) {
              throw new Error('When loading deps, got ' +
                              e.stack ? e.stack : e.message);
            }
            delete base.addModuleStylesheet;
            delete base.addModuleRawScriptDependency;
            delete base.addModuleDependency;
          }
        
          // TODO(dsinclair): Remove this when HTML imports land as the templates
          // will be pulled in by the requireTemplate calls.
          var templatesLoaded_ = false;
          function ensureTemplatesLoaded() {
            if (templatesLoaded_ || window.FLATTENED)
              return;
            templatesLoaded_ = true;
        
            var req = new XMLHttpRequest();
            req.open('GET', '/templates', false);
            req.send(null);
        
            var elem = document.createElement('div');
            elem.innerHTML = req.responseText;
            while (elem.hasChildNodes())
              document.head.appendChild(elem.removeChild(elem.firstChild));
          }
        
          var moduleLoadStatus = {};
          var rawScriptLoadStatus = {};
          function require(modules, opt_indentLevel) {
            var indentLevel = opt_indentLevel || 0;
            var dependentModules = modules;
            if (!(modules instanceof Array))
              dependentModules = [modules];
        
            ensureDepsLoaded();
            ensureTemplatesLoaded();
        
            dependentModules.forEach(function(module) {
              requireModule(module, indentLevel);
            });
          }
        
          var modulesWaiting = [];
          function requireModule(dependentModuleName, indentLevel) {
            if (window.FLATTENED) {
              if (!window.FLATTENED[dependentModuleName]) {
                throw new Error('Somehow, module ' + dependentModuleName +
                                ' didn\'t get stored in the flattened js file! ' +
                                'You may need to rerun ' +
                                'build/generate_about_tracing_contents.py');
              }
              return;
            }
        
            if (moduleLoadStatus[dependentModuleName] == 'APPENDED')
              return;
        
            if (moduleLoadStatus[dependentModuleName] == 'RESOLVING')
              return;
        
            mLog('require(' + dependentModuleName + ')', indentLevel);
            moduleLoadStatus[dependentModuleName] = 'RESOLVING';
            requireDependencies(dependentModuleName, indentLevel);
        
            loadScript(dependentModuleName.replace(/\./g, '/') + '.js');
            moduleLoadStatus[name] = 'APPENDED';
          }
        
          function requireDependencies(dependentModuleName, indentLevel) {
            // Load the module's dependent scripts after.
            var dependentModules = moduleDependencies[dependentModuleName] || [];
            require(dependentModules, indentLevel + 1);
        
            // Load the module stylesheet first.
            var stylesheets = moduleStylesheets[dependentModuleName] || [];
            for (var i = 0; i < stylesheets.length; i++)
              requireStylesheet(stylesheets[i]);
        
            // Load the module raw scripts next
            var rawScripts = moduleRawScripts[dependentModuleName] || [];
            for (var i = 0; i < rawScripts.length; i++) {
              var rawScriptName = rawScripts[i];
              if (rawScriptLoadStatus[rawScriptName])
                continue;
        
              loadScript(rawScriptName);
              mLog('load(' + rawScriptName + ')', indentLevel);
              rawScriptLoadStatus[rawScriptName] = 'APPENDED';
            }
          }
        
          function loadScript(path) {
            var scriptEl = document.createElement('script');
            scriptEl.src = moduleBasePath + '/' + path;
            scriptEl.type = 'text/javascript';
            scriptEl.defer = true;
            scriptEl.async = false;
            base.doc.head.appendChild(scriptEl);
          }
        
          /**
           * Adds a dependency on a raw javascript file, e.g. a third party
           * library.
           * @param {String} rawScriptName The path to the script file, relative to
           * moduleBasePath.
           */
          function requireRawScript(rawScriptPath) {
            if (window.FLATTENED_RAW_SCRIPTS) {
              if (!window.FLATTENED_RAW_SCRIPTS[rawScriptPath]) {
                throw new Error('Somehow, ' + rawScriptPath +
                    ' didn\'t get stored in the flattened js file! ' +
                    'You may need to rerun build/generate_about_tracing_contents.py');
              }
              return;
            }
        
            if (rawScriptLoadStatus[rawScriptPath])
              return;
            throw new Error(rawScriptPath + ' should already have been loaded.' +
                ' Did you forget to run build/generate_about_tracing_contents.py?');
          }
        
          var stylesheetLoadStatus = {};
          function requireStylesheet(dependentStylesheetName) {
            if (window.FLATTENED)
              return;
        
            if (stylesheetLoadStatus[dependentStylesheetName])
              return;
            stylesheetLoadStatus[dependentStylesheetName] = true;
        
            var localPath = dependentStylesheetName.replace(/\./g, '/') + '.css';
            var stylesheetPath = moduleBasePath + '/' + localPath;
        
            var linkEl = document.createElement('link');
            linkEl.setAttribute('rel', 'stylesheet');
            linkEl.setAttribute('href', stylesheetPath);
            base.doc.head.appendChild(linkEl);
          }
        
          var templateLoadStatus = {};
          function requireTemplate(template) {
            if (window.FLATTENED)
              return;
        
            if (templateLoadStatus[template])
              return;
            templateLoadStatus[template] = true;
        
            var localPath = template.replace(/\./g, '/') + '.html';
            var importPath = moduleBasePath + '/' + localPath;
        
            var linkEl = document.createElement('link');
            linkEl.setAttribute('rel', 'import');
            linkEl.setAttribute('href', importPath);
            // TODO(dsinclair): Enable when HTML imports are available.
            //base.doc.head.appendChild(linkEl);
          }
        
          function exportTo(namespace, fn) {
            var obj = exportPath(namespace);
            try {
              var exports = fn();
            } catch (e) {
              console.log('While running exports for ', namespace, ':');
              console.log(e.stack || e);
              return;
            }
        
            for (var propertyName in exports) {
              // Maybe we should check the prototype chain here? The current usage
              // pattern is always using an object literal so we only care about own
              // properties.
              var propertyDescriptor = Object.getOwnPropertyDescriptor(exports,
                                                                       propertyName);
              if (propertyDescriptor) {
                Object.defineProperty(obj, propertyName, propertyDescriptor);
                mLog('  +' + propertyName);
              }
            }
          };
        
          /**
           * Initialization which must be deferred until run-time.
           */
          function initialize() {
            // If 'document' isn't defined, then we must be being pre-compiled,
            // so set a trap so that we're initialized on first access at run-time.
            if (!global.document) {
              var originalBase = base;
        
              Object.defineProperty(global, 'base', {
                get: function() {
                  Object.defineProperty(global, 'base', {value: originalBase});
                  originalBase.initialize();
                  return originalBase;
                },
                configurable: true
              });
        
              return;
            }
        
            base.doc = document;
        
            base.isMac = /Mac/.test(navigator.platform);
            base.isWindows = /Win/.test(navigator.platform);
            base.isChromeOS = /CrOS/.test(navigator.userAgent);
            base.isLinux = /Linux/.test(navigator.userAgent);
            base.isGTK = /GTK/.test(chrome.toolkit);
            base.isViews = /views/.test(chrome.toolkit);
        
            setModuleBasePath('/src');
          }
        
          return {
            set moduleBasePath(path) {
              setModuleBasePath(path);
            },
        
            get moduleBasePath() {
              return moduleBasePath;
            },
        
            initialize: initialize,
        
            require: require,
            requireStylesheet: requireStylesheet,
            requireRawScript: requireRawScript,
            requireTemplate: requireTemplate,
            exportTo: exportTo
          };
        })();
        
        base.initialize();
        
        // Copyright (c) 2012 The Chromium Authors. All rights reserved.
        // Use of this source code is governed by a BSD-style license that can be
        // found in the LICENSE file.
        
        'use strict';
        
        /**
         * @fileoverview This contains an implementation of the EventTarget interface
         * as defined by DOM Level 2 Events.
         */
        base.exportTo('base', function() {
        
          /**
           * Creates a new EventTarget. This class implements the DOM level 2
           * EventTarget interface and can be used wherever those are used.
           * @constructor
           */
          function EventTarget() {
          }
        
          EventTarget.prototype = {
        
            /**
             * Adds an event listener to the target.
             * @param {string} type The name of the event.
             * @param {!Function|{handleEvent:Function}} handler The handler for the
             *     event. This is called when the event is dispatched.
             */
            addEventListener: function(type, handler) {
              if (!this.listeners_)
                this.listeners_ = Object.create(null);
              if (!(type in this.listeners_)) {
                this.listeners_[type] = [handler];
              } else {
                var handlers = this.listeners_[type];
                if (handlers.indexOf(handler) < 0)
                  handlers.push(handler);
              }
            },
        
            /**
             * Removes an event listener from the target.
             * @param {string} type The name of the event.
             * @param {!Function|{handleEvent:Function}} handler The handler for the
             *     event.
             */
            removeEventListener: function(type, handler) {
              if (!this.listeners_)
                return;
              if (type in this.listeners_) {
                var handlers = this.listeners_[type];
                var index = handlers.indexOf(handler);
                if (index >= 0) {
                  // Clean up if this was the last listener.
                  if (handlers.length == 1)
                    delete this.listeners_[type];
                  else
                    handlers.splice(index, 1);
                }
              }
            },
        
            /**
             * Dispatches an event and calls all the listeners that are listening to
             * the type of the event.
             * @param {!cr.event.Event} event The event to dispatch.
             * @return {boolean} Whether the default action was prevented. If someone
             *     calls preventDefault on the event object then this returns false.
             */
            dispatchEvent: function(event) {
              if (!this.listeners_)
                return true;
        
              // Since we are using DOM Event objects we need to override some of the
              // properties and methods so that we can emulate this correctly.
              var self = this;
              event.__defineGetter__('target', function() {
                return self;
              });
              var realPreventDefault = event.preventDefault;
              event.preventDefault = function() {
                realPreventDefault.call(this);
                this.rawReturnValue = false;
              };
        
              var type = event.type;
              var prevented = 0;
              if (type in this.listeners_) {
                // Clone to prevent removal during dispatch
                var handlers = this.listeners_[type].concat();
                for (var i = 0, handler; handler = handlers[i]; i++) {
                  if (handler.handleEvent)
                    prevented |= handler.handleEvent.call(handler, event) === false;
                  else
                    prevented |= handler.call(this, event) === false;
                }
              }
        
              return !prevented && event.rawReturnValue;
            },
        
            hasEventListener: function(type) {
              return this.listeners_[type] !== undefined;
            }
          };
        
          var EventTargetHelper = {
            decorate: function(target) {
              for (var k in EventTargetHelper) {
                if (k == 'decorate')
                  continue;
                var v = EventTargetHelper[k];
                if (typeof v !== 'function')
                  continue;
                target[k] = v;
              }
              target.listenerCounts_ = {};
            },
        
            addEventListener: function(type, listener, useCapture) {
              this.__proto__.addEventListener.call(
                  this, type, listener, useCapture);
              if (this.listenerCounts_[type] === undefined)
                this.listenerCounts_[type] = 0;
              this.listenerCounts_[type]++;
            },
        
            removeEventListener: function(type, listener, useCapture) {
              this.__proto__.removeEventListener.call(
                  this, type, listener, useCapture);
              this.listenerCounts_[type]--;
            },
        
            hasEventListener: function(type) {
              return this.listenerCounts_[type] > 0;
            }
          };
        
          // Export
          return {
            EventTarget: EventTarget,
            EventTargetHelper: EventTargetHelper
          };
        });
        
        // Copyright (c) 2013 The Chromium Authors. All rights reserved.
        // Use of this source code is governed by a BSD-style license that can be
        // found in the LICENSE file.
        
        'use strict';
        
        base.require('base.event_target');
        
        base.exportTo('base', function() {
          /**
           * Creates a new event to be used with base.EventTarget or DOM EventTarget
           * objects.
           * @param {string} type The name of the event.
           * @param {boolean=} opt_bubbles Whether the event bubbles.
           *     Default is false.
           * @param {boolean=} opt_preventable Whether the default action of the event
           *     can be prevented.
           * @constructor
           * @extends {Event}
           */
          function Event(type, opt_bubbles, opt_preventable) {
            var e = base.doc.createEvent('Event');
            e.initEvent(type, !!opt_bubbles, !!opt_preventable);
            e.__proto__ = global.Event.prototype;
            return e;
          };
        
          Event.prototype = {
            __proto__: global.Event.prototype
          };
        
          /**
           * Dispatches a simple event on an event target.
           * @param {!EventTarget} target The event target to dispatch the event on.
           * @param {string} type The type of the event.
           * @param {boolean=} opt_bubbles Whether the event bubbles or not.
           * @param {boolean=} opt_cancelable Whether the default action of the event
           *     can be prevented.
           * @return {boolean} If any of the listeners called {@code preventDefault}
           *     during the dispatch this will return false.
           */
          function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) {
            var e = new Event(type, opt_bubbles, opt_cancelable);
            return target.dispatchEvent(e);
          }
        
          return {
            Event: Event,
            dispatchSimpleEvent: dispatchSimpleEvent
          };
        });
        
        
        // Copyright (c) 2013 The Chromium Authors. All rights reserved.
        // Use of this source code is governed by a BSD-style license that can be
        // found in the LICENSE file.
        
        'use strict';
        
        base.require('base.events');
        
        base.exportTo('base', function() {
          /**
           * Fires a property change event on the target.
           * @param {EventTarget} target The target to dispatch the event on.
           * @param {string} propertyName The name of the property that changed.
           * @param {*} newValue The new value for the property.
           * @param {*} oldValue The old value for the property.
           */
          function dispatchPropertyChange(target, propertyName, newValue, oldValue,
                                          opt_bubbles, opt_cancelable) {
            var e = new base.Event(propertyName + 'Change',
                                   opt_bubbles, opt_cancelable);
            e.propertyName = propertyName;
            e.newValue = newValue;
            e.oldValue = oldValue;
        
            var error;
            e.throwError = function(err) {  // workaround CR 239648
              error = err;
            };
        
            target.dispatchEvent(e);
            if (error)
              throw error;
          }
        
          function setPropertyAndDispatchChange(obj, propertyName, newValue) {
            var privateName = propertyName + '_';
            var oldValue = obj[propertyName];
            obj[privateName] = newValue;
            if (oldValue !== newValue)
              base.dispatchPropertyChange(obj, propertyName,
                  newValue, oldValue, true, false);
          }
        
          /**
           * Converts a camelCase javascript property name to a hyphenated-lower-case
           * attribute name.
           * @param {string} jsName The javascript camelCase property name.
           * @return {string} The equivalent hyphenated-lower-case attribute name.
           */
          function getAttributeName(jsName) {
            return jsName.replace(/([A-Z])/g, '-$1').toLowerCase();
          }
        
          /* Creates a private name unlikely to collide with object properties names
           * @param {string} name The defineProperty name
           * @return {string} an obfuscated name
           */
          function getPrivateName(name) {
            return name + '_base_';
          }
        
          /**
           * The kind of property to define in {@code defineProperty}.
           * @enum {number}
           * @const
           */
          var PropertyKind = {
            /**
             * Plain old JS property where the backing data is stored as a 'private'
             * field on the object.
             */
            JS: 'js',
        
            /**
             * The property backing data is stored as an attribute on an element.
             */
            ATTR: 'attr',
        
            /**
             * The property backing data is stored as an attribute on an element. If the
             * element has the attribute then the value is true.
             */
            BOOL_ATTR: 'boolAttr'
          };
        
          /**
           * Helper function for defineProperty that returns the getter to use for the
           * property.
           * @param {string} name The name of the property.
           * @param {base.PropertyKind} kind The kind of the property.
           * @return {function():*} The getter for the property.
           */
          function getGetter(name, kind) {
            switch (kind) {
              case PropertyKind.JS:
                var privateName = getPrivateName(name);
                return function() {
                  return this[privateName];
                };
              case PropertyKind.ATTR:
                var attributeName = getAttributeName(name);
                return function() {
                  return this.getAttribute(attributeName);
                };
              case PropertyKind.BOOL_ATTR:
                var attributeName = getAttributeName(name);
                return function() {
                  return this.hasAttribute(attributeName);
                };
            }
          }
        
          /**
           * Helper function for defineProperty that returns the setter of the right
           * kind.
           * @param {string} name The name of the property we are defining the setter
           *     for.
           * @param {base.PropertyKind} kind The kind of property we are getting the
           *     setter for.
           * @param {function(*):void=} opt_setHook A function to run after the property
           *     is set, but before the propertyChange event is fired.
           * @param {boolean=} opt_bubbles Whether the event bubbles or not.
           * @param {boolean=} opt_cancelable Whether the default action of the event
           *     can be prevented.
           * @return {function(*):void} The function to use as a setter.
           */
          function getSetter(name, kind, opt_setHook, opt_bubbles, opt_cancelable) {
            switch (kind) {
              case PropertyKind.JS:
                var privateName = getPrivateName(name);
                return function(value) {
                  var oldValue = this[privateName];
                  if (value !== oldValue) {
                    this[privateName] = value;
                    if (opt_setHook)
                      opt_setHook.call(this, value, oldValue);
                    dispatchPropertyChange(this, name, value, oldValue,
                        opt_bubbles, opt_cancelable);
                  }
                };
        
              case PropertyKind.ATTR:
                var attributeName = getAttributeName(name);
                return function(value) {
                  var oldValue = this.getAttribute(attributeName);
                  if (value !== oldValue) {
                    if (value == undefined)
                      this.removeAttribute(attributeName);
                    else
                      this.setAttribute(attributeName, value);
                    if (opt_setHook)
                      opt_setHook.call(this, value, oldValue);
                    dispatchPropertyChange(this, name, value, oldValue,
                        opt_bubbles, opt_cancelable);
                  }
                };
        
              case PropertyKind.BOOL_ATTR:
                var attributeName = getAttributeName(name);
                return function(value) {
                  var oldValue = (this.getAttribute(attributeName) === name);
                  if (value !== oldValue) {
                    if (value)
                      this.setAttribute(attributeName, name);
                    else
                      this.removeAttribute(attributeName);
                    if (opt_setHook)
                      opt_setHook.call(this, value, oldValue);
                    dispatchPropertyChange(this, name, value, oldValue,
                        opt_bubbles, opt_cancelable);
                  }
                };
            }
          }
        
          /**
           * Defines a property on an object. When the setter changes the value a
           * property change event with the type {@code name + 'Change'} is fired.
           * @param {!Object} obj The object to define the property for.
           * @param {string} name The name of the property.
           * @param {base.PropertyKind=} opt_kind What kind of underlying storage to
           * use.
           * @param {function(*):void=} opt_setHook A function to run after the
           *     property is set, but before the propertyChange event is fired.
           * @param {boolean=} opt_bubbles Whether the event bubbles or not.
           * @param {boolean=} opt_cancelable Whether the default action of the event
           *     can be prevented.
           */
          function defineProperty(obj, name, opt_kind, opt_setHook,
                                  opt_bubbles, opt_cancelable) {
            console.error("Don't use base.defineProperty");
            if (typeof obj == 'function')
              obj = obj.prototype;
        
            var kind = opt_kind || PropertyKind.JS;
        
            if (!obj.__lookupGetter__(name))
              obj.__defineGetter__(name, getGetter(name, kind));
        
            if (!obj.__lookupSetter__(name))
              obj.__defineSetter__(name, getSetter(name, kind, opt_setHook,
                  opt_bubbles, opt_cancelable));
          }
        
          return {
            PropertyKind: PropertyKind,
            defineProperty: defineProperty,
            dispatchPropertyChange: dispatchPropertyChange,
            setPropertyAndDispatchChange: setPropertyAndDispatchChange
          };
        });
        
        // Copyright (c) 2013 The Chromium Authors. All rights reserved.
        // Use of this source code is governed by a BSD-style license that can be
        // found in the LICENSE file.
        
        'use strict';
        
        base.exportTo('base', function() {
          function asArray(arrayish) {
            var values = [];
            for (var i = 0; i < arrayish.length; i++)
              values.push(arrayish[i]);
            return values;
          }
        
          function compareArrays(x, y, elementCmp) {
            var minLength = Math.min(x.length, y.length);
            for (var i = 0; i < minLength; i++) {
              var tmp = elementCmp(x[i], y[i]);
              if (tmp)
                return tmp;
            }
            if (x.length == y.length)
              return 0;
        
            if (x[i] === undefined)
              return -1;
        
            return 1;
          }
        
          /**
           * Compares two values when one or both might be undefined. Undefined
           * values are sorted after defined.
           */
          function comparePossiblyUndefinedValues(x, y, cmp) {
            if (x !== undefined && y !== undefined)
              return cmp(x, y);
            if (x !== undefined)
              return -1;
            if (y !== undefined)
              return 1;
            return 0;
          }
        
          function concatenateArrays(/*arguments*/) {
            var values = [];
            for (var i = 0; i < arguments.length; i++) {
              if (!(arguments[i] instanceof Array))
                throw new Error('Arguments ' + i + 'is not an array');
              values.push.apply(values, arguments[i]);
            }
            return values;
          }
        
          function dictionaryKeys(dict) {
            var keys = [];
            for (var key in dict)
              keys.push(key);
            return keys;
          }
        
          function dictionaryValues(dict) {
            var values = [];
            for (var key in dict)
              values.push(dict[key]);
            return values;
          }
        
          function iterItems(dict, fn, opt_this) {
            opt_this = opt_this || this;
            for (var key in dict)
              fn.call(opt_this, key, dict[key]);
          }
        
          function iterObjectFieldsRecursively(object, func) {
            if (!(object instanceof Object))
              return;
        
            if (object instanceof Array) {
              for (var i = 0; i < object.length; i++) {
                func(object, i, object[i]);
                iterObjectFieldsRecursively(object[i], func);
              }
              return;
            }
        
            for (var key in object) {
              var value = object[key];
              func(object, key, value);
              iterObjectFieldsRecursively(value, func);
            }
          }
        
          return {
            asArray: asArray,
            concatenateArrays: concatenateArrays,
            compareArrays: compareArrays,
            comparePossiblyUndefinedValues: comparePossiblyUndefinedValues,
            dictionaryKeys: dictionaryKeys,
            dictionaryValues: dictionaryValues,
            iterItems: iterItems,
            iterObjectFieldsRecursively: iterObjectFieldsRecursively
          };
        });
        
        /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
        
        Redistribution and use in source and binary forms, with or without modification,
        are permitted provided that the following conditions are met:
        
          * Redistributions of source code must retain the above copyright notice, this
            list of conditions and the following disclaimer.
          * Redistributions in binary form must reproduce the above copyright notice,
            this list of conditions and the following disclaimer in the documentation 
            and/or other materials provided with the distribution.
        
        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
        ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
        WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
        DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
        ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
        (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
        LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
        ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
        (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
        SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
        
        if(!GLMAT_EPSILON) {
            var GLMAT_EPSILON = 0.000001;
        }
        
        if(!GLMAT_ARRAY_TYPE) {
            var GLMAT_ARRAY_TYPE = (typeof Float32Array !== 'undefined') ? Float32Array : Array;
        }
        
        /**
         * @class Common utilities
         * @name glMatrix
         */
        var glMatrix = {};
        
        /**
         * Sets the type of array used when creating new vectors and matricies
         *
         * @param {Type} type Array type, such as Float32Array or Array
         */
        glMatrix.setMatrixArrayType = function(type) {
            GLMAT_ARRAY_TYPE = type;
        }
        
        if(typeof(exports) !== 'undefined') {
            exports.glMatrix = glMatrix;
        }
        /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved.
        
        Redistribution and use in source and binary forms, with or without modification,
        are permitted provided that the following conditions are met:
        
          * Redistributions of source code must retain the above copyright notice, this
            list of conditions and the following disclaimer.
          * Redistributions in binary form must reproduce the above copyright notice,
            this list of conditions and the following disclaimer in the documentation 
            and/or other materials provided with the distribution.
        
        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
        ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
        WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
        DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
        ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
        (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
        LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
        ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
        (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
        SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
        
        /**
         * @class 2x3 Matrix
         * @name mat2d
         * 
         * @description 
         * A mat2d contains six elements defined as:
         * 
         * [a, b,
         *  c, d,
         *  tx,ty]
         * 
        * This is a short form for the 3x3 matrix: *
         * [a, b, 0
         *  c, d, 0
         *  tx,ty,1]
         * 
        * The last column is ignored so the array is shorter and operations are faster. */ var mat2d = {}; /** * Creates a new identity mat2d * * @returns {mat2d} a new 2x3 matrix */ mat2d.create = function() { var out = new GLMAT_ARRAY_TYPE(6); out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 1; out[4] = 0; out[5] = 0; return out; }; /** * Creates a new mat2d initialized with values from an existing matrix * * @param {mat2d} a matrix to clone * @returns {mat2d} a new 2x3 matrix */ mat2d.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(6); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; return out; }; /** * Copy the values from one mat2d to another * * @param {mat2d} out the receiving matrix * @param {mat2d} a the source matrix * @returns {mat2d} out */ mat2d.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; return out; }; /** * Set a mat2d to the identity matrix * * @param {mat2d} out the receiving matrix * @returns {mat2d} out */ mat2d.identity = function(out) { out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 1; out[4] = 0; out[5] = 0; return out; }; /** * Inverts a mat2d * * @param {mat2d} out the receiving matrix * @param {mat2d} a the source matrix * @returns {mat2d} out */ mat2d.invert = function(out, a) { var aa = a[0], ab = a[1], ac = a[2], ad = a[3], atx = a[4], aty = a[5]; var det = aa * ad - ab * ac; if(!det){ return null; } det = 1.0 / det; out[0] = ad * det; out[1] = -ab * det; out[2] = -ac * det; out[3] = aa * det; out[4] = (ac * aty - ad * atx) * det; out[5] = (ab * atx - aa * aty) * det; return out; }; /** * Calculates the determinant of a mat2d * * @param {mat2d} a the source matrix * @returns {Number} determinant of a */ mat2d.determinant = function (a) { return a[0] * a[3] - a[1] * a[2]; }; /** * Multiplies two mat2d's * * @param {mat2d} out the receiving matrix * @param {mat2d} a the first operand * @param {mat2d} b the second operand * @returns {mat2d} out */ mat2d.multiply = function (out, a, b) { var aa = a[0], ab = a[1], ac = a[2], ad = a[3], atx = a[4], aty = a[5], ba = b[0], bb = b[1], bc = b[2], bd = b[3], btx = b[4], bty = b[5]; out[0] = aa*ba + ab*bc; out[1] = aa*bb + ab*bd; out[2] = ac*ba + ad*bc; out[3] = ac*bb + ad*bd; out[4] = ba*atx + bc*aty + btx; out[5] = bb*atx + bd*aty + bty; return out; }; /** * Alias for {@link mat2d.multiply} * @function */ mat2d.mul = mat2d.multiply; /** * Rotates a mat2d by the given angle * * @param {mat2d} out the receiving matrix * @param {mat2d} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @returns {mat2d} out */ mat2d.rotate = function (out, a, rad) { var aa = a[0], ab = a[1], ac = a[2], ad = a[3], atx = a[4], aty = a[5], st = Math.sin(rad), ct = Math.cos(rad); out[0] = aa*ct + ab*st; out[1] = -aa*st + ab*ct; out[2] = ac*ct + ad*st; out[3] = -ac*st + ct*ad; out[4] = ct*atx + st*aty; out[5] = ct*aty - st*atx; return out; }; /** * Scales the mat2d by the dimensions in the given vec2 * * @param {mat2d} out the receiving matrix * @param {mat2d} a the matrix to translate * @param {mat2d} v the vec2 to scale the matrix by * @returns {mat2d} out **/ mat2d.scale = function(out, a, v) { var vx = v[0], vy = v[1]; out[0] = a[0] * vx; out[1] = a[1] * vy; out[2] = a[2] * vx; out[3] = a[3] * vy; out[4] = a[4] * vx; out[5] = a[5] * vy; return out; }; /** * Translates the mat2d by the dimensions in the given vec2 * * @param {mat2d} out the receiving matrix * @param {mat2d} a the matrix to translate * @param {mat2d} v the vec2 to translate the matrix by * @returns {mat2d} out **/ mat2d.translate = function(out, a, v) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4] + v[0]; out[5] = a[5] + v[1]; return out; }; /** * Returns a string representation of a mat2d * * @param {mat2d} a matrix to represent as a string * @returns {String} string representation of the matrix */ mat2d.str = function (a) { return 'mat2d(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ', ' + a[4] + ', ' + a[5] + ')'; }; if(typeof(exports) !== 'undefined') { exports.mat2d = mat2d; } /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 4x4 Matrix * @name mat4 */ var mat4 = {}; /** * Creates a new identity mat4 * * @returns {mat4} a new 4x4 matrix */ mat4.create = function() { var out = new GLMAT_ARRAY_TYPE(16); out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = 1; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = 1; out[11] = 0; out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1; return out; }; /** * Creates a new mat4 initialized with values from an existing matrix * * @param {mat4} a matrix to clone * @returns {mat4} a new 4x4 matrix */ mat4.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(16); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[8] = a[8]; out[9] = a[9]; out[10] = a[10]; out[11] = a[11]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; return out; }; /** * Copy the values from one mat4 to another * * @param {mat4} out the receiving matrix * @param {mat4} a the source matrix * @returns {mat4} out */ mat4.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[8] = a[8]; out[9] = a[9]; out[10] = a[10]; out[11] = a[11]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; return out; }; /** * Set a mat4 to the identity matrix * * @param {mat4} out the receiving matrix * @returns {mat4} out */ mat4.identity = function(out) { out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = 1; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = 1; out[11] = 0; out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1; return out; }; /** * Transpose the values of a mat4 * * @param {mat4} out the receiving matrix * @param {mat4} a the source matrix * @returns {mat4} out */ mat4.transpose = function(out, a) { // If we are transposing ourselves we can skip a few steps but have to cache some values if (out === a) { var a01 = a[1], a02 = a[2], a03 = a[3], a12 = a[6], a13 = a[7], a23 = a[11]; out[1] = a[4]; out[2] = a[8]; out[3] = a[12]; out[4] = a01; out[6] = a[9]; out[7] = a[13]; out[8] = a02; out[9] = a12; out[11] = a[14]; out[12] = a03; out[13] = a13; out[14] = a23; } else { out[0] = a[0]; out[1] = a[4]; out[2] = a[8]; out[3] = a[12]; out[4] = a[1]; out[5] = a[5]; out[6] = a[9]; out[7] = a[13]; out[8] = a[2]; out[9] = a[6]; out[10] = a[10]; out[11] = a[14]; out[12] = a[3]; out[13] = a[7]; out[14] = a[11]; out[15] = a[15]; } return out; }; /** * Inverts a mat4 * * @param {mat4} out the receiving matrix * @param {mat4} a the source matrix * @returns {mat4} out */ mat4.invert = function(out, a) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11, b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30, b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32, // Calculate the determinant det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; if (!det) { return null; } det = 1.0 / det; out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; return out; }; /** * Calculates the adjugate of a mat4 * * @param {mat4} out the receiving matrix * @param {mat4} a the source matrix * @returns {mat4} out */ mat4.adjoint = function(out, a) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; out[0] = (a11 * (a22 * a33 - a23 * a32) - a21 * (a12 * a33 - a13 * a32) + a31 * (a12 * a23 - a13 * a22)); out[1] = -(a01 * (a22 * a33 - a23 * a32) - a21 * (a02 * a33 - a03 * a32) + a31 * (a02 * a23 - a03 * a22)); out[2] = (a01 * (a12 * a33 - a13 * a32) - a11 * (a02 * a33 - a03 * a32) + a31 * (a02 * a13 - a03 * a12)); out[3] = -(a01 * (a12 * a23 - a13 * a22) - a11 * (a02 * a23 - a03 * a22) + a21 * (a02 * a13 - a03 * a12)); out[4] = -(a10 * (a22 * a33 - a23 * a32) - a20 * (a12 * a33 - a13 * a32) + a30 * (a12 * a23 - a13 * a22)); out[5] = (a00 * (a22 * a33 - a23 * a32) - a20 * (a02 * a33 - a03 * a32) + a30 * (a02 * a23 - a03 * a22)); out[6] = -(a00 * (a12 * a33 - a13 * a32) - a10 * (a02 * a33 - a03 * a32) + a30 * (a02 * a13 - a03 * a12)); out[7] = (a00 * (a12 * a23 - a13 * a22) - a10 * (a02 * a23 - a03 * a22) + a20 * (a02 * a13 - a03 * a12)); out[8] = (a10 * (a21 * a33 - a23 * a31) - a20 * (a11 * a33 - a13 * a31) + a30 * (a11 * a23 - a13 * a21)); out[9] = -(a00 * (a21 * a33 - a23 * a31) - a20 * (a01 * a33 - a03 * a31) + a30 * (a01 * a23 - a03 * a21)); out[10] = (a00 * (a11 * a33 - a13 * a31) - a10 * (a01 * a33 - a03 * a31) + a30 * (a01 * a13 - a03 * a11)); out[11] = -(a00 * (a11 * a23 - a13 * a21) - a10 * (a01 * a23 - a03 * a21) + a20 * (a01 * a13 - a03 * a11)); out[12] = -(a10 * (a21 * a32 - a22 * a31) - a20 * (a11 * a32 - a12 * a31) + a30 * (a11 * a22 - a12 * a21)); out[13] = (a00 * (a21 * a32 - a22 * a31) - a20 * (a01 * a32 - a02 * a31) + a30 * (a01 * a22 - a02 * a21)); out[14] = -(a00 * (a11 * a32 - a12 * a31) - a10 * (a01 * a32 - a02 * a31) + a30 * (a01 * a12 - a02 * a11)); out[15] = (a00 * (a11 * a22 - a12 * a21) - a10 * (a01 * a22 - a02 * a21) + a20 * (a01 * a12 - a02 * a11)); return out; }; /** * Calculates the determinant of a mat4 * * @param {mat4} a the source matrix * @returns {Number} determinant of a */ mat4.determinant = function (a) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], b00 = a00 * a11 - a01 * a10, b01 = a00 * a12 - a02 * a10, b02 = a00 * a13 - a03 * a10, b03 = a01 * a12 - a02 * a11, b04 = a01 * a13 - a03 * a11, b05 = a02 * a13 - a03 * a12, b06 = a20 * a31 - a21 * a30, b07 = a20 * a32 - a22 * a30, b08 = a20 * a33 - a23 * a30, b09 = a21 * a32 - a22 * a31, b10 = a21 * a33 - a23 * a31, b11 = a22 * a33 - a23 * a32; // Calculate the determinant return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; }; /** * Multiplies two mat4's * * @param {mat4} out the receiving matrix * @param {mat4} a the first operand * @param {mat4} b the second operand * @returns {mat4} out */ mat4.multiply = function (out, a, b) { var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; // Cache only the current line of the second matrix var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33; b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30; out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31; out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32; out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33; return out; }; /** * Alias for {@link mat4.multiply} * @function */ mat4.mul = mat4.multiply; /** * Translate a mat4 by the given vector * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to translate * @param {vec3} v vector to translate by * @returns {mat4} out */ mat4.translate = function (out, a, v) { var x = v[0], y = v[1], z = v[2], a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23; if (a === out) { out[12] = a[0] * x + a[4] * y + a[8] * z + a[12]; out[13] = a[1] * x + a[5] * y + a[9] * z + a[13]; out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; } else { a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03; out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13; out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23; out[12] = a00 * x + a10 * y + a20 * z + a[12]; out[13] = a01 * x + a11 * y + a21 * z + a[13]; out[14] = a02 * x + a12 * y + a22 * z + a[14]; out[15] = a03 * x + a13 * y + a23 * z + a[15]; } return out; }; /** * Scales the mat4 by the dimensions in the given vec3 * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to scale * @param {vec3} v the vec3 to scale the matrix by * @returns {mat4} out **/ mat4.scale = function(out, a, v) { var x = v[0], y = v[1], z = v[2]; out[0] = a[0] * x; out[1] = a[1] * x; out[2] = a[2] * x; out[3] = a[3] * x; out[4] = a[4] * y; out[5] = a[5] * y; out[6] = a[6] * y; out[7] = a[7] * y; out[8] = a[8] * z; out[9] = a[9] * z; out[10] = a[10] * z; out[11] = a[11] * z; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; return out; }; /** * Rotates a mat4 by the given angle * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @param {vec3} axis the axis to rotate around * @returns {mat4} out */ mat4.rotate = function (out, a, rad, axis) { var x = axis[0], y = axis[1], z = axis[2], len = Math.sqrt(x * x + y * y + z * z), s, c, t, a00, a01, a02, a03, a10, a11, a12, a13, a20, a21, a22, a23, b00, b01, b02, b10, b11, b12, b20, b21, b22; if (Math.abs(len) < GLMAT_EPSILON) { return null; } len = 1 / len; x *= len; y *= len; z *= len; s = Math.sin(rad); c = Math.cos(rad); t = 1 - c; a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; // Construct the elements of the rotation matrix b00 = x * x * t + c; b01 = y * x * t + z * s; b02 = z * x * t - y * s; b10 = x * y * t - z * s; b11 = y * y * t + c; b12 = z * y * t + x * s; b20 = x * z * t + y * s; b21 = y * z * t - x * s; b22 = z * z * t + c; // Perform rotation-specific matrix multiplication out[0] = a00 * b00 + a10 * b01 + a20 * b02; out[1] = a01 * b00 + a11 * b01 + a21 * b02; out[2] = a02 * b00 + a12 * b01 + a22 * b02; out[3] = a03 * b00 + a13 * b01 + a23 * b02; out[4] = a00 * b10 + a10 * b11 + a20 * b12; out[5] = a01 * b10 + a11 * b11 + a21 * b12; out[6] = a02 * b10 + a12 * b11 + a22 * b12; out[7] = a03 * b10 + a13 * b11 + a23 * b12; out[8] = a00 * b20 + a10 * b21 + a20 * b22; out[9] = a01 * b20 + a11 * b21 + a21 * b22; out[10] = a02 * b20 + a12 * b21 + a22 * b22; out[11] = a03 * b20 + a13 * b21 + a23 * b22; if (a !== out) { // If the source and destination differ, copy the unchanged last row out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } return out; }; /** * Rotates a matrix by the given angle around the X axis * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @returns {mat4} out */ mat4.rotateX = function (out, a, rad) { var s = Math.sin(rad), c = Math.cos(rad), a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; if (a !== out) { // If the source and destination differ, copy the unchanged rows out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } // Perform axis-specific matrix multiplication out[4] = a10 * c + a20 * s; out[5] = a11 * c + a21 * s; out[6] = a12 * c + a22 * s; out[7] = a13 * c + a23 * s; out[8] = a20 * c - a10 * s; out[9] = a21 * c - a11 * s; out[10] = a22 * c - a12 * s; out[11] = a23 * c - a13 * s; return out; }; /** * Rotates a matrix by the given angle around the Y axis * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @returns {mat4} out */ mat4.rotateY = function (out, a, rad) { var s = Math.sin(rad), c = Math.cos(rad), a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11]; if (a !== out) { // If the source and destination differ, copy the unchanged rows out[4] = a[4]; out[5] = a[5]; out[6] = a[6]; out[7] = a[7]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } // Perform axis-specific matrix multiplication out[0] = a00 * c - a20 * s; out[1] = a01 * c - a21 * s; out[2] = a02 * c - a22 * s; out[3] = a03 * c - a23 * s; out[8] = a00 * s + a20 * c; out[9] = a01 * s + a21 * c; out[10] = a02 * s + a22 * c; out[11] = a03 * s + a23 * c; return out; }; /** * Rotates a matrix by the given angle around the Z axis * * @param {mat4} out the receiving matrix * @param {mat4} a the matrix to rotate * @param {Number} rad the angle to rotate the matrix by * @returns {mat4} out */ mat4.rotateZ = function (out, a, rad) { var s = Math.sin(rad), c = Math.cos(rad), a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7]; if (a !== out) { // If the source and destination differ, copy the unchanged last row out[8] = a[8]; out[9] = a[9]; out[10] = a[10]; out[11] = a[11]; out[12] = a[12]; out[13] = a[13]; out[14] = a[14]; out[15] = a[15]; } // Perform axis-specific matrix multiplication out[0] = a00 * c + a10 * s; out[1] = a01 * c + a11 * s; out[2] = a02 * c + a12 * s; out[3] = a03 * c + a13 * s; out[4] = a10 * c - a00 * s; out[5] = a11 * c - a01 * s; out[6] = a12 * c - a02 * s; out[7] = a13 * c - a03 * s; return out; }; /** * Creates a matrix from a quaternion rotation and vector translation * This is equivalent to (but much faster than): * * mat4.identity(dest); * mat4.translate(dest, vec); * var quatMat = mat4.create(); * quat4.toMat4(quat, quatMat); * mat4.multiply(dest, quatMat); * * @param {mat4} out mat4 receiving operation result * @param {quat4} q Rotation quaternion * @param {vec3} v Translation vector * @returns {mat4} out */ mat4.fromRotationTranslation = function (out, q, v) { // Quaternion math var x = q[0], y = q[1], z = q[2], w = q[3], x2 = x + x, y2 = y + y, z2 = z + z, xx = x * x2, xy = x * y2, xz = x * z2, yy = y * y2, yz = y * z2, zz = z * z2, wx = w * x2, wy = w * y2, wz = w * z2; out[0] = 1 - (yy + zz); out[1] = xy + wz; out[2] = xz - wy; out[3] = 0; out[4] = xy - wz; out[5] = 1 - (xx + zz); out[6] = yz + wx; out[7] = 0; out[8] = xz + wy; out[9] = yz - wx; out[10] = 1 - (xx + yy); out[11] = 0; out[12] = v[0]; out[13] = v[1]; out[14] = v[2]; out[15] = 1; return out; }; /** * Calculates a 4x4 matrix from the given quaternion * * @param {mat4} out mat4 receiving operation result * @param {quat} q Quaternion to create matrix from * * @returns {mat4} out */ mat4.fromQuat = function (out, q) { var x = q[0], y = q[1], z = q[2], w = q[3], x2 = x + x, y2 = y + y, z2 = z + z, xx = x * x2, xy = x * y2, xz = x * z2, yy = y * y2, yz = y * z2, zz = z * z2, wx = w * x2, wy = w * y2, wz = w * z2; out[0] = 1 - (yy + zz); out[1] = xy + wz; out[2] = xz - wy; out[3] = 0; out[4] = xy - wz; out[5] = 1 - (xx + zz); out[6] = yz + wx; out[7] = 0; out[8] = xz + wy; out[9] = yz - wx; out[10] = 1 - (xx + yy); out[11] = 0; out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1; return out; }; /** * Generates a frustum matrix with the given bounds * * @param {mat4} out mat4 frustum matrix will be written into * @param {Number} left Left bound of the frustum * @param {Number} right Right bound of the frustum * @param {Number} bottom Bottom bound of the frustum * @param {Number} top Top bound of the frustum * @param {Number} near Near bound of the frustum * @param {Number} far Far bound of the frustum * @returns {mat4} out */ mat4.frustum = function (out, left, right, bottom, top, near, far) { var rl = 1 / (right - left), tb = 1 / (top - bottom), nf = 1 / (near - far); out[0] = (near * 2) * rl; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = (near * 2) * tb; out[6] = 0; out[7] = 0; out[8] = (right + left) * rl; out[9] = (top + bottom) * tb; out[10] = (far + near) * nf; out[11] = -1; out[12] = 0; out[13] = 0; out[14] = (far * near * 2) * nf; out[15] = 0; return out; }; /** * Generates a perspective projection matrix with the given bounds * * @param {mat4} out mat4 frustum matrix will be written into * @param {number} fovy Vertical field of view in radians * @param {number} aspect Aspect ratio. typically viewport width/height * @param {number} near Near bound of the frustum * @param {number} far Far bound of the frustum * @returns {mat4} out */ mat4.perspective = function (out, fovy, aspect, near, far) { var f = 1.0 / Math.tan(fovy / 2), nf = 1 / (near - far); out[0] = f / aspect; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = f; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = (far + near) * nf; out[11] = -1; out[12] = 0; out[13] = 0; out[14] = (2 * far * near) * nf; out[15] = 0; return out; }; /** * Generates a orthogonal projection matrix with the given bounds * * @param {mat4} out mat4 frustum matrix will be written into * @param {number} left Left bound of the frustum * @param {number} right Right bound of the frustum * @param {number} bottom Bottom bound of the frustum * @param {number} top Top bound of the frustum * @param {number} near Near bound of the frustum * @param {number} far Far bound of the frustum * @returns {mat4} out */ mat4.ortho = function (out, left, right, bottom, top, near, far) { var lr = 1 / (left - right), bt = 1 / (bottom - top), nf = 1 / (near - far); out[0] = -2 * lr; out[1] = 0; out[2] = 0; out[3] = 0; out[4] = 0; out[5] = -2 * bt; out[6] = 0; out[7] = 0; out[8] = 0; out[9] = 0; out[10] = 2 * nf; out[11] = 0; out[12] = (left + right) * lr; out[13] = (top + bottom) * bt; out[14] = (far + near) * nf; out[15] = 1; return out; }; /** * Generates a look-at matrix with the given eye position, focal point, and up axis * * @param {mat4} out mat4 frustum matrix will be written into * @param {vec3} eye Position of the viewer * @param {vec3} center Point the viewer is looking at * @param {vec3} up vec3 pointing up * @returns {mat4} out */ mat4.lookAt = function (out, eye, center, up) { var x0, x1, x2, y0, y1, y2, z0, z1, z2, len, eyex = eye[0], eyey = eye[1], eyez = eye[2], upx = up[0], upy = up[1], upz = up[2], centerx = center[0], centery = center[1], centerz = center[2]; if (Math.abs(eyex - centerx) < GLMAT_EPSILON && Math.abs(eyey - centery) < GLMAT_EPSILON && Math.abs(eyez - centerz) < GLMAT_EPSILON) { return mat4.identity(out); } z0 = eyex - centerx; z1 = eyey - centery; z2 = eyez - centerz; len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2); z0 *= len; z1 *= len; z2 *= len; x0 = upy * z2 - upz * z1; x1 = upz * z0 - upx * z2; x2 = upx * z1 - upy * z0; len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2); if (!len) { x0 = 0; x1 = 0; x2 = 0; } else { len = 1 / len; x0 *= len; x1 *= len; x2 *= len; } y0 = z1 * x2 - z2 * x1; y1 = z2 * x0 - z0 * x2; y2 = z0 * x1 - z1 * x0; len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2); if (!len) { y0 = 0; y1 = 0; y2 = 0; } else { len = 1 / len; y0 *= len; y1 *= len; y2 *= len; } out[0] = x0; out[1] = y0; out[2] = z0; out[3] = 0; out[4] = x1; out[5] = y1; out[6] = z1; out[7] = 0; out[8] = x2; out[9] = y2; out[10] = z2; out[11] = 0; out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); out[15] = 1; return out; }; /** * Returns a string representation of a mat4 * * @param {mat4} mat matrix to represent as a string * @returns {String} string representation of the matrix */ mat4.str = function (a) { return 'mat4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ', ' + a[4] + ', ' + a[5] + ', ' + a[6] + ', ' + a[7] + ', ' + a[8] + ', ' + a[9] + ', ' + a[10] + ', ' + a[11] + ', ' + a[12] + ', ' + a[13] + ', ' + a[14] + ', ' + a[15] + ')'; }; if(typeof(exports) !== 'undefined') { exports.mat4 = mat4; } /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 2 Dimensional Vector * @name vec2 */ var vec2 = {}; /** * Creates a new, empty vec2 * * @returns {vec2} a new 2D vector */ vec2.create = function() { var out = new GLMAT_ARRAY_TYPE(2); out[0] = 0; out[1] = 0; return out; }; /** * Creates a new vec2 initialized with values from an existing vector * * @param {vec2} a vector to clone * @returns {vec2} a new 2D vector */ vec2.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(2); out[0] = a[0]; out[1] = a[1]; return out; }; /** * Creates a new vec2 initialized with the given values * * @param {Number} x X component * @param {Number} y Y component * @returns {vec2} a new 2D vector */ vec2.fromValues = function(x, y) { var out = new GLMAT_ARRAY_TYPE(2); out[0] = x; out[1] = y; return out; }; /** * Copy the values from one vec2 to another * * @param {vec2} out the receiving vector * @param {vec2} a the source vector * @returns {vec2} out */ vec2.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; return out; }; /** * Set the components of a vec2 to the given values * * @param {vec2} out the receiving vector * @param {Number} x X component * @param {Number} y Y component * @returns {vec2} out */ vec2.set = function(out, x, y) { out[0] = x; out[1] = y; return out; }; /** * Adds two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.add = function(out, a, b) { out[0] = a[0] + b[0]; out[1] = a[1] + b[1]; return out; }; /** * Subtracts two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.subtract = function(out, a, b) { out[0] = a[0] - b[0]; out[1] = a[1] - b[1]; return out; }; /** * Alias for {@link vec2.subtract} * @function */ vec2.sub = vec2.subtract; /** * Multiplies two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.multiply = function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; return out; }; /** * Alias for {@link vec2.multiply} * @function */ vec2.mul = vec2.multiply; /** * Divides two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.divide = function(out, a, b) { out[0] = a[0] / b[0]; out[1] = a[1] / b[1]; return out; }; /** * Alias for {@link vec2.divide} * @function */ vec2.div = vec2.divide; /** * Returns the minimum of two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.min = function(out, a, b) { out[0] = Math.min(a[0], b[0]); out[1] = Math.min(a[1], b[1]); return out; }; /** * Returns the maximum of two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec2} out */ vec2.max = function(out, a, b) { out[0] = Math.max(a[0], b[0]); out[1] = Math.max(a[1], b[1]); return out; }; /** * Scales a vec2 by a scalar number * * @param {vec2} out the receiving vector * @param {vec2} a the vector to scale * @param {Number} b amount to scale the vector by * @returns {vec2} out */ vec2.scale = function(out, a, b) { out[0] = a[0] * b; out[1] = a[1] * b; return out; }; /** * Calculates the euclidian distance between two vec2's * * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {Number} distance between a and b */ vec2.distance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1]; return Math.sqrt(x*x + y*y); }; /** * Alias for {@link vec2.distance} * @function */ vec2.dist = vec2.distance; /** * Calculates the squared euclidian distance between two vec2's * * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {Number} squared distance between a and b */ vec2.squaredDistance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1]; return x*x + y*y; }; /** * Alias for {@link vec2.squaredDistance} * @function */ vec2.sqrDist = vec2.squaredDistance; /** * Calculates the length of a vec2 * * @param {vec2} a vector to calculate length of * @returns {Number} length of a */ vec2.length = function (a) { var x = a[0], y = a[1]; return Math.sqrt(x*x + y*y); }; /** * Alias for {@link vec2.length} * @function */ vec2.len = vec2.length; /** * Calculates the squared length of a vec2 * * @param {vec2} a vector to calculate squared length of * @returns {Number} squared length of a */ vec2.squaredLength = function (a) { var x = a[0], y = a[1]; return x*x + y*y; }; /** * Alias for {@link vec2.squaredLength} * @function */ vec2.sqrLen = vec2.squaredLength; /** * Negates the components of a vec2 * * @param {vec2} out the receiving vector * @param {vec2} a vector to negate * @returns {vec2} out */ vec2.negate = function(out, a) { out[0] = -a[0]; out[1] = -a[1]; return out; }; /** * Normalize a vec2 * * @param {vec2} out the receiving vector * @param {vec2} a vector to normalize * @returns {vec2} out */ vec2.normalize = function(out, a) { var x = a[0], y = a[1]; var len = x*x + y*y; if (len > 0) { //TODO: evaluate use of glm_invsqrt here? len = 1 / Math.sqrt(len); out[0] = a[0] * len; out[1] = a[1] * len; } return out; }; /** * Calculates the dot product of two vec2's * * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {Number} dot product of a and b */ vec2.dot = function (a, b) { return a[0] * b[0] + a[1] * b[1]; }; /** * Computes the cross product of two vec2's * Note that the cross product must by definition produce a 3D vector * * @param {vec3} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @returns {vec3} out */ vec2.cross = function(out, a, b) { var z = a[0] * b[1] - a[1] * b[0]; out[0] = out[1] = 0; out[2] = z; return out; }; /** * Performs a linear interpolation between two vec2's * * @param {vec2} out the receiving vector * @param {vec2} a the first operand * @param {vec2} b the second operand * @param {Number} t interpolation amount between the two inputs * @returns {vec2} out */ vec2.lerp = function (out, a, b, t) { var ax = a[0], ay = a[1]; out[0] = ax + t * (b[0] - ax); out[1] = ay + t * (b[1] - ay); return out; }; /** * Transforms the vec2 with a mat2 * * @param {vec2} out the receiving vector * @param {vec2} a the vector to transform * @param {mat2} m matrix to transform with * @returns {vec2} out */ vec2.transformMat2 = function(out, a, m) { var x = a[0], y = a[1]; out[0] = m[0] * x + m[2] * y; out[1] = m[1] * x + m[3] * y; return out; }; /** * Transforms the vec2 with a mat2d * * @param {vec2} out the receiving vector * @param {vec2} a the vector to transform * @param {mat2d} m matrix to transform with * @returns {vec2} out */ vec2.transformMat2d = function(out, a, m) { var x = a[0], y = a[1]; out[0] = m[0] * x + m[2] * y + m[4]; out[1] = m[1] * x + m[3] * y + m[5]; return out; }; /** * Transforms the vec2 with a mat3 * 3rd vector component is implicitly '1' * * @param {vec2} out the receiving vector * @param {vec2} a the vector to transform * @param {mat3} m matrix to transform with * @returns {vec2} out */ vec2.transformMat3 = function(out, a, m) { var x = a[0], y = a[1]; out[0] = m[0] * x + m[3] * y + m[6]; out[1] = m[1] * x + m[4] * y + m[7]; return out; }; /** * Transforms the vec2 with a mat4 * 3rd vector component is implicitly '0' * 4th vector component is implicitly '1' * * @param {vec2} out the receiving vector * @param {vec2} a the vector to transform * @param {mat4} m matrix to transform with * @returns {vec2} out */ vec2.transformMat4 = function(out, a, m) { var x = a[0], y = a[1]; out[0] = m[0] * x + m[4] * y + m[12]; out[1] = m[1] * x + m[5] * y + m[13]; return out; }; /** * Perform some operation over an array of vec2s. * * @param {Array} a the array of vectors to iterate over * @param {Number} stride Number of elements between the start of each vec2. If 0 assumes tightly packed * @param {Number} offset Number of elements to skip at the beginning of the array * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array * @param {Function} fn Function to call for each vector in the array * @param {Object} [arg] additional argument to pass to fn * @returns {Array} a * @function */ vec2.forEach = (function() { var vec = vec2.create(); return function(a, stride, offset, count, fn, arg) { var i, l; if(!stride) { stride = 2; } if(!offset) { offset = 0; } if(count) { l = Math.min((count * stride) + offset, a.length); } else { l = a.length; } for(i = offset; i < l; i += stride) { vec[0] = a[i]; vec[1] = a[i+1]; fn(vec, vec, arg); a[i] = vec[0]; a[i+1] = vec[1]; } return a; }; })(); /** * Returns a string representation of a vector * * @param {vec2} vec vector to represent as a string * @returns {String} string representation of the vector */ vec2.str = function (a) { return 'vec2(' + a[0] + ', ' + a[1] + ')'; }; if(typeof(exports) !== 'undefined') { exports.vec2 = vec2; } /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 3 Dimensional Vector * @name vec3 */ var vec3 = {}; /** * Creates a new, empty vec3 * * @returns {vec3} a new 3D vector */ vec3.create = function() { var out = new GLMAT_ARRAY_TYPE(3); out[0] = 0; out[1] = 0; out[2] = 0; return out; }; /** * Creates a new vec3 initialized with values from an existing vector * * @param {vec3} a vector to clone * @returns {vec3} a new 3D vector */ vec3.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(3); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; return out; }; /** * Creates a new vec3 initialized with the given values * * @param {Number} x X component * @param {Number} y Y component * @param {Number} z Z component * @returns {vec3} a new 3D vector */ vec3.fromValues = function(x, y, z) { var out = new GLMAT_ARRAY_TYPE(3); out[0] = x; out[1] = y; out[2] = z; return out; }; /** * Copy the values from one vec3 to another * * @param {vec3} out the receiving vector * @param {vec3} a the source vector * @returns {vec3} out */ vec3.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; return out; }; /** * Set the components of a vec3 to the given values * * @param {vec3} out the receiving vector * @param {Number} x X component * @param {Number} y Y component * @param {Number} z Z component * @returns {vec3} out */ vec3.set = function(out, x, y, z) { out[0] = x; out[1] = y; out[2] = z; return out; }; /** * Adds two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.add = function(out, a, b) { out[0] = a[0] + b[0]; out[1] = a[1] + b[1]; out[2] = a[2] + b[2]; return out; }; /** * Subtracts two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.subtract = function(out, a, b) { out[0] = a[0] - b[0]; out[1] = a[1] - b[1]; out[2] = a[2] - b[2]; return out; }; /** * Alias for {@link vec3.subtract} * @function */ vec3.sub = vec3.subtract; /** * Multiplies two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.multiply = function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; out[2] = a[2] * b[2]; return out; }; /** * Alias for {@link vec3.multiply} * @function */ vec3.mul = vec3.multiply; /** * Divides two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.divide = function(out, a, b) { out[0] = a[0] / b[0]; out[1] = a[1] / b[1]; out[2] = a[2] / b[2]; return out; }; /** * Alias for {@link vec3.divide} * @function */ vec3.div = vec3.divide; /** * Returns the minimum of two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.min = function(out, a, b) { out[0] = Math.min(a[0], b[0]); out[1] = Math.min(a[1], b[1]); out[2] = Math.min(a[2], b[2]); return out; }; /** * Returns the maximum of two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.max = function(out, a, b) { out[0] = Math.max(a[0], b[0]); out[1] = Math.max(a[1], b[1]); out[2] = Math.max(a[2], b[2]); return out; }; /** * Scales a vec3 by a scalar number * * @param {vec3} out the receiving vector * @param {vec3} a the vector to scale * @param {Number} b amount to scale the vector by * @returns {vec3} out */ vec3.scale = function(out, a, b) { out[0] = a[0] * b; out[1] = a[1] * b; out[2] = a[2] * b; return out; }; /** * Calculates the euclidian distance between two vec3's * * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {Number} distance between a and b */ vec3.distance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1], z = b[2] - a[2]; return Math.sqrt(x*x + y*y + z*z); }; /** * Alias for {@link vec3.distance} * @function */ vec3.dist = vec3.distance; /** * Calculates the squared euclidian distance between two vec3's * * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {Number} squared distance between a and b */ vec3.squaredDistance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1], z = b[2] - a[2]; return x*x + y*y + z*z; }; /** * Alias for {@link vec3.squaredDistance} * @function */ vec3.sqrDist = vec3.squaredDistance; /** * Calculates the length of a vec3 * * @param {vec3} a vector to calculate length of * @returns {Number} length of a */ vec3.length = function (a) { var x = a[0], y = a[1], z = a[2]; return Math.sqrt(x*x + y*y + z*z); }; /** * Alias for {@link vec3.length} * @function */ vec3.len = vec3.length; /** * Calculates the squared length of a vec3 * * @param {vec3} a vector to calculate squared length of * @returns {Number} squared length of a */ vec3.squaredLength = function (a) { var x = a[0], y = a[1], z = a[2]; return x*x + y*y + z*z; }; /** * Alias for {@link vec3.squaredLength} * @function */ vec3.sqrLen = vec3.squaredLength; /** * Negates the components of a vec3 * * @param {vec3} out the receiving vector * @param {vec3} a vector to negate * @returns {vec3} out */ vec3.negate = function(out, a) { out[0] = -a[0]; out[1] = -a[1]; out[2] = -a[2]; return out; }; /** * Normalize a vec3 * * @param {vec3} out the receiving vector * @param {vec3} a vector to normalize * @returns {vec3} out */ vec3.normalize = function(out, a) { var x = a[0], y = a[1], z = a[2]; var len = x*x + y*y + z*z; if (len > 0) { //TODO: evaluate use of glm_invsqrt here? len = 1 / Math.sqrt(len); out[0] = a[0] * len; out[1] = a[1] * len; out[2] = a[2] * len; } return out; }; /** * Calculates the dot product of two vec3's * * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {Number} dot product of a and b */ vec3.dot = function (a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }; /** * Computes the cross product of two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @returns {vec3} out */ vec3.cross = function(out, a, b) { var ax = a[0], ay = a[1], az = a[2], bx = b[0], by = b[1], bz = b[2]; out[0] = ay * bz - az * by; out[1] = az * bx - ax * bz; out[2] = ax * by - ay * bx; return out; }; /** * Performs a linear interpolation between two vec3's * * @param {vec3} out the receiving vector * @param {vec3} a the first operand * @param {vec3} b the second operand * @param {Number} t interpolation amount between the two inputs * @returns {vec3} out */ vec3.lerp = function (out, a, b, t) { var ax = a[0], ay = a[1], az = a[2]; out[0] = ax + t * (b[0] - ax); out[1] = ay + t * (b[1] - ay); out[2] = az + t * (b[2] - az); return out; }; /** * Transforms the vec3 with a mat4. * 4th vector component is implicitly '1' * * @param {vec3} out the receiving vector * @param {vec3} a the vector to transform * @param {mat4} m matrix to transform with * @returns {vec3} out */ vec3.transformMat4 = function(out, a, m) { var x = a[0], y = a[1], z = a[2]; out[0] = m[0] * x + m[4] * y + m[8] * z + m[12]; out[1] = m[1] * x + m[5] * y + m[9] * z + m[13]; out[2] = m[2] * x + m[6] * y + m[10] * z + m[14]; return out; }; /** * Transforms the vec3 with a quat * * @param {vec3} out the receiving vector * @param {vec3} a the vector to transform * @param {quat} q quaternion to transform with * @returns {vec3} out */ vec3.transformQuat = function(out, a, q) { var x = a[0], y = a[1], z = a[2], qx = q[0], qy = q[1], qz = q[2], qw = q[3], // calculate quat * vec ix = qw * x + qy * z - qz * y, iy = qw * y + qz * x - qx * z, iz = qw * z + qx * y - qy * x, iw = -qx * x - qy * y - qz * z; // calculate result * inverse quat out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; return out; }; /** * Perform some operation over an array of vec3s. * * @param {Array} a the array of vectors to iterate over * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed * @param {Number} offset Number of elements to skip at the beginning of the array * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array * @param {Function} fn Function to call for each vector in the array * @param {Object} [arg] additional argument to pass to fn * @returns {Array} a * @function */ vec3.forEach = (function() { var vec = vec3.create(); return function(a, stride, offset, count, fn, arg) { var i, l; if(!stride) { stride = 3; } if(!offset) { offset = 0; } if(count) { l = Math.min((count * stride) + offset, a.length); } else { l = a.length; } for(i = offset; i < l; i += stride) { vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; fn(vec, vec, arg); a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; } return a; }; })(); /** * Returns a string representation of a vector * * @param {vec3} vec vector to represent as a string * @returns {String} string representation of the vector */ vec3.str = function (a) { return 'vec3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ')'; }; if(typeof(exports) !== 'undefined') { exports.vec3 = vec3; } /* Copyright (c) 2013, Brandon Jones, Colin MacKenzie IV. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @class 4 Dimensional Vector * @name vec4 */ var vec4 = {}; /** * Creates a new, empty vec4 * * @returns {vec4} a new 4D vector */ vec4.create = function() { var out = new GLMAT_ARRAY_TYPE(4); out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 0; return out; }; /** * Creates a new vec4 initialized with values from an existing vector * * @param {vec4} a vector to clone * @returns {vec4} a new 4D vector */ vec4.clone = function(a) { var out = new GLMAT_ARRAY_TYPE(4); out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out; }; /** * Creates a new vec4 initialized with the given values * * @param {Number} x X component * @param {Number} y Y component * @param {Number} z Z component * @param {Number} w W component * @returns {vec4} a new 4D vector */ vec4.fromValues = function(x, y, z, w) { var out = new GLMAT_ARRAY_TYPE(4); out[0] = x; out[1] = y; out[2] = z; out[3] = w; return out; }; /** * Copy the values from one vec4 to another * * @param {vec4} out the receiving vector * @param {vec4} a the source vector * @returns {vec4} out */ vec4.copy = function(out, a) { out[0] = a[0]; out[1] = a[1]; out[2] = a[2]; out[3] = a[3]; return out; }; /** * Set the components of a vec4 to the given values * * @param {vec4} out the receiving vector * @param {Number} x X component * @param {Number} y Y component * @param {Number} z Z component * @param {Number} w W component * @returns {vec4} out */ vec4.set = function(out, x, y, z, w) { out[0] = x; out[1] = y; out[2] = z; out[3] = w; return out; }; /** * Adds two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.add = function(out, a, b) { out[0] = a[0] + b[0]; out[1] = a[1] + b[1]; out[2] = a[2] + b[2]; out[3] = a[3] + b[3]; return out; }; /** * Subtracts two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.subtract = function(out, a, b) { out[0] = a[0] - b[0]; out[1] = a[1] - b[1]; out[2] = a[2] - b[2]; out[3] = a[3] - b[3]; return out; }; /** * Alias for {@link vec4.subtract} * @function */ vec4.sub = vec4.subtract; /** * Multiplies two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.multiply = function(out, a, b) { out[0] = a[0] * b[0]; out[1] = a[1] * b[1]; out[2] = a[2] * b[2]; out[3] = a[3] * b[3]; return out; }; /** * Alias for {@link vec4.multiply} * @function */ vec4.mul = vec4.multiply; /** * Divides two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.divide = function(out, a, b) { out[0] = a[0] / b[0]; out[1] = a[1] / b[1]; out[2] = a[2] / b[2]; out[3] = a[3] / b[3]; return out; }; /** * Alias for {@link vec4.divide} * @function */ vec4.div = vec4.divide; /** * Returns the minimum of two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.min = function(out, a, b) { out[0] = Math.min(a[0], b[0]); out[1] = Math.min(a[1], b[1]); out[2] = Math.min(a[2], b[2]); out[3] = Math.min(a[3], b[3]); return out; }; /** * Returns the maximum of two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {vec4} out */ vec4.max = function(out, a, b) { out[0] = Math.max(a[0], b[0]); out[1] = Math.max(a[1], b[1]); out[2] = Math.max(a[2], b[2]); out[3] = Math.max(a[3], b[3]); return out; }; /** * Scales a vec4 by a scalar number * * @param {vec4} out the receiving vector * @param {vec4} a the vector to scale * @param {Number} b amount to scale the vector by * @returns {vec4} out */ vec4.scale = function(out, a, b) { out[0] = a[0] * b; out[1] = a[1] * b; out[2] = a[2] * b; out[3] = a[3] * b; return out; }; /** * Calculates the euclidian distance between two vec4's * * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {Number} distance between a and b */ vec4.distance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1], z = b[2] - a[2], w = b[3] - a[3]; return Math.sqrt(x*x + y*y + z*z + w*w); }; /** * Alias for {@link vec4.distance} * @function */ vec4.dist = vec4.distance; /** * Calculates the squared euclidian distance between two vec4's * * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {Number} squared distance between a and b */ vec4.squaredDistance = function(a, b) { var x = b[0] - a[0], y = b[1] - a[1], z = b[2] - a[2], w = b[3] - a[3]; return x*x + y*y + z*z + w*w; }; /** * Alias for {@link vec4.squaredDistance} * @function */ vec4.sqrDist = vec4.squaredDistance; /** * Calculates the length of a vec4 * * @param {vec4} a vector to calculate length of * @returns {Number} length of a */ vec4.length = function (a) { var x = a[0], y = a[1], z = a[2], w = a[3]; return Math.sqrt(x*x + y*y + z*z + w*w); }; /** * Alias for {@link vec4.length} * @function */ vec4.len = vec4.length; /** * Calculates the squared length of a vec4 * * @param {vec4} a vector to calculate squared length of * @returns {Number} squared length of a */ vec4.squaredLength = function (a) { var x = a[0], y = a[1], z = a[2], w = a[3]; return x*x + y*y + z*z + w*w; }; /** * Alias for {@link vec4.squaredLength} * @function */ vec4.sqrLen = vec4.squaredLength; /** * Negates the components of a vec4 * * @param {vec4} out the receiving vector * @param {vec4} a vector to negate * @returns {vec4} out */ vec4.negate = function(out, a) { out[0] = -a[0]; out[1] = -a[1]; out[2] = -a[2]; out[3] = -a[3]; return out; }; /** * Normalize a vec4 * * @param {vec4} out the receiving vector * @param {vec4} a vector to normalize * @returns {vec4} out */ vec4.normalize = function(out, a) { var x = a[0], y = a[1], z = a[2], w = a[3]; var len = x*x + y*y + z*z + w*w; if (len > 0) { len = 1 / Math.sqrt(len); out[0] = a[0] * len; out[1] = a[1] * len; out[2] = a[2] * len; out[3] = a[3] * len; } return out; }; /** * Calculates the dot product of two vec4's * * @param {vec4} a the first operand * @param {vec4} b the second operand * @returns {Number} dot product of a and b */ vec4.dot = function (a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; }; /** * Performs a linear interpolation between two vec4's * * @param {vec4} out the receiving vector * @param {vec4} a the first operand * @param {vec4} b the second operand * @param {Number} t interpolation amount between the two inputs * @returns {vec4} out */ vec4.lerp = function (out, a, b, t) { var ax = a[0], ay = a[1], az = a[2], aw = a[3]; out[0] = ax + t * (b[0] - ax); out[1] = ay + t * (b[1] - ay); out[2] = az + t * (b[2] - az); out[3] = aw + t * (b[3] - aw); return out; }; /** * Transforms the vec4 with a mat4. * * @param {vec4} out the receiving vector * @param {vec4} a the vector to transform * @param {mat4} m matrix to transform with * @returns {vec4} out */ vec4.transformMat4 = function(out, a, m) { var x = a[0], y = a[1], z = a[2], w = a[3]; out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w; out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w; out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w; out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w; return out; }; /** * Transforms the vec4 with a quat * * @param {vec4} out the receiving vector * @param {vec4} a the vector to transform * @param {quat} q quaternion to transform with * @returns {vec4} out */ vec4.transformQuat = function(out, a, q) { var x = a[0], y = a[1], z = a[2], qx = q[0], qy = q[1], qz = q[2], qw = q[3], // calculate quat * vec ix = qw * x + qy * z - qz * y, iy = qw * y + qz * x - qx * z, iz = qw * z + qx * y - qy * x, iw = -qx * x - qy * y - qz * z; // calculate result * inverse quat out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy; out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz; out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx; return out; }; /** * Perform some operation over an array of vec4s. * * @param {Array} a the array of vectors to iterate over * @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed * @param {Number} offset Number of elements to skip at the beginning of the array * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array * @param {Function} fn Function to call for each vector in the array * @param {Object} [arg] additional argument to pass to fn * @returns {Array} a * @function */ vec4.forEach = (function() { var vec = vec4.create(); return function(a, stride, offset, count, fn, arg) { var i, l; if(!stride) { stride = 4; } if(!offset) { offset = 0; } if(count) { l = Math.min((count * stride) + offset, a.length); } else { l = a.length; } for(i = offset; i < l; i += stride) { vec[0] = a[i]; vec[1] = a[i+1]; vec[2] = a[i+2]; vec[3] = a[i+3]; fn(vec, vec, arg); a[i] = vec[0]; a[i+1] = vec[1]; a[i+2] = vec[2]; a[i+3] = vec[3]; } return a; }; })(); /** * Returns a string representation of a vector * * @param {vec4} vec vector to represent as a string * @returns {String} string representation of the vector */ vec4.str = function (a) { return 'vec4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ')'; }; if(typeof(exports) !== 'undefined') { exports.vec4 = vec4; } // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireRawScript('../third_party/gl-matrix/src/gl-matrix/common.js'); base.requireRawScript('../third_party/gl-matrix/src/gl-matrix/mat2d.js'); base.requireRawScript('../third_party/gl-matrix/src/gl-matrix/mat4.js'); base.requireRawScript('../third_party/gl-matrix/src/gl-matrix/vec2.js'); base.requireRawScript('../third_party/gl-matrix/src/gl-matrix/vec3.js'); base.requireRawScript('../third_party/gl-matrix/src/gl-matrix/vec4.js'); base.exportTo('base', function() { var tmp_vec2 = vec2.create(); var tmp_vec2b = vec2.create(); var tmp_vec4 = vec4.create(); var tmp_mat2d = mat2d.create(); vec2.createFromArray = function(arr) { if (arr.length != 2) throw new Error('Should be length 2'); var v = vec2.create(); vec2.set(v, arr[0], arr[1]); return v; }; vec2.createXY = function(x, y) { var v = vec2.create(); vec2.set(v, x, y); return v; }; vec2.toString = function(a) { return '[' + a[0] + ', ' + a[1] + ']'; }; vec2.addTwoScaledUnitVectors = function(out, u1, scale1, u2, scale2) { // out = u1 * scale1 + u2 * scale2 vec2.scale(tmp_vec2, u1, scale1); vec2.scale(tmp_vec2b, u2, scale2); vec2.add(out, tmp_vec2, tmp_vec2b); } vec3.createXYZ = function(x, y, z) { var v = vec3.create(); vec3.set(v, x, y, z); return v; }; vec3.toString = function(a) { return 'vec3(' + a[0] + ', ' + a[1] + ', ' + a[2] + ')'; } mat2d.translateXY = function(out, x, y) { vec2.set(tmp_vec2, x, y); mat2d.translate(out, out, tmp_vec2); } mat2d.scaleXY = function(out, x, y) { vec2.set(tmp_vec2, x, y); mat2d.scale(out, out, tmp_vec2); } vec4.unitize = function(out, a) { out[0] = a[0] / a[3]; out[1] = a[1] / a[3]; out[2] = a[2] / a[3]; out[3] = 1; return out; } vec2.copyFromVec4 = function(out, a) { vec4.unitize(tmp_vec4, a); vec2.copy(out, tmp_vec4); } return {}; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview 2D Rectangle math. */ base.require('base.gl_matrix'); base.exportTo('base', function() { /** * Tracks a 2D bounding box. * @constructor */ function Rect() { this.x = 0; this.y = 0; this.width = 0; this.height = 0; }; Rect.fromXYWH = function(x, y, w, h) { var rect = new Rect(); rect.x = x; rect.y = y; rect.width = w; rect.height = h; return rect; } Rect.fromArray = function(ary) { if (ary.length != 4) throw new Error('ary.length must be 4'); var rect = new Rect(); rect.x = ary[0]; rect.y = ary[1]; rect.width = ary[2]; rect.height = ary[3]; return rect; } Rect.prototype = { __proto__: Object.prototype, get left() { return this.x; }, get top() { return this.y; }, get right() { return this.x + this.width; }, get bottom() { return this.y + this.height; }, toString: function() { return 'Rect(' + this.x + ', ' + this.y + ', ' + this.width + ', ' + this.height + ')'; }, toArray: function() { return [this.x, this.y, this.width, this.height]; }, clone: function() { var rect = new Rect(); rect.x = this.x; rect.y = this.y; rect.width = this.width; rect.height = this.height; return rect; }, enlarge: function(pad) { var rect = new Rect(); this.enlargeFast(rect, pad); return rect; }, enlargeFast: function(out, pad) { out.x = this.x - pad; out.y = this.y - pad; out.width = this.width + 2 * pad; out.height = this.height + 2 * pad; return out; }, size: function() { return {width: this.width, height: this.height}; }, scale: function(s) { var rect = new Rect(); this.scaleFast(rect, s); return rect; }, scaleSize: function(s) { return Rect.fromXYWH(this.x, this.y, this.width * s, this.height * s); }, scaleFast: function(out, s) { out.x = this.x * s; out.y = this.y * s; out.width = this.width * s; out.height = this.height * s; return out; }, translate: function(v) { var rect = new Rect(); this.translateFast(rect, v); return rect; }, translateFast: function(out, v) { out.x = this.x + v[0]; out.y = this.x + v[1]; out.width = this.width; out.height = this.height; return out; }, asUVRectInside: function(containingRect) { var rect = new Rect(); rect.x = (this.x - containingRect.x) / containingRect.width; rect.y = (this.y - containingRect.y) / containingRect.height; rect.width = this.width / containingRect.width; rect.height = this.height / containingRect.height; return rect; }, intersects: function(that) { var ok = true; ok &= this.x < that.right; ok &= this.right > that.x; ok &= this.y < that.bottom; ok &= this.bottom > that.y; return ok; }, equalTo: function(rect) { return rect && (this.x === rect.x) && (this.y === rect.y) && (this.width === rect.width) && (this.height === rect.height); } }; return { Rect: Rect }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.iteration_helpers'); base.require('base.rect'); base.exportTo('base', function() { /** * Adds a {@code getInstance} static method that always return the same * instance object. * @param {!Function} ctor The constructor for the class to add the static * method to. */ function addSingletonGetter(ctor) { ctor.getInstance = function() { return ctor.instance_ || (ctor.instance_ = new ctor()); }; } function instantiateTemplate(selector) { return document.querySelector(selector).content.cloneNode(true); } function tracedFunction(fn, name, opt_this) { function F() { console.time(name); try { fn.apply(opt_this, arguments); } finally { console.timeEnd(name); } } return F; } function normalizeException(e) { if (typeof(e) == 'string') { return { message: e, stack: [''] }; } return { message: e.message, stack: e.stack ? e.stack : [''] }; } function stackTrace() { var stack = new Error().stack + ''; stack = stack.split('\n'); return stack.slice(2); } function windowRectForElement(element) { var position = [element.offsetLeft, element.offsetTop]; var size = [element.offsetWidth, element.offsetHeight]; var node = element.offsetParent; while (node) { position[0] += node.offsetLeft; position[1] += node.offsetTop; node = node.offsetParent; } return base.Rect.fromXYWH(position[0], position[1], size[0], size[1]); } function clamp(x, lo, hi) { return Math.min(Math.max(x, lo), hi); } function lerp(percentage, lo, hi) { var range = hi - lo; return lo + percentage * range; } function deg2rad(deg) { return (Math.PI * deg) / 180.0; } function scrollIntoViewIfNeeded(el) { var pr = el.parentElement.getBoundingClientRect(); var cr = el.getBoundingClientRect(); if (cr.top < pr.top) { el.scrollIntoView(true); } else if (cr.bottom > pr.bottom) { el.scrollIntoView(false); } } return { addSingletonGetter: addSingletonGetter, tracedFunction: tracedFunction, normalizeException: normalizeException, instantiateTemplate: instantiateTemplate, stackTrace: stackTrace, windowRectForElement: windowRectForElement, scrollIntoViewIfNeeded: scrollIntoViewIfNeeded, clamp: clamp, lerp: lerp, deg2rad: deg2rad }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.exportTo('ui', function() { /** * Decorates elements as an instance of a class. * @param {string|!Element} source The way to find the element(s) to decorate. * If this is a string then {@code querySeletorAll} is used to find the * elements to decorate. * @param {!Function} constr The constructor to decorate with. The constr * needs to have a {@code decorate} function. */ function decorate(source, constr) { var elements; if (typeof source == 'string') elements = base.doc.querySelectorAll(source); else elements = [source]; for (var i = 0, el; el = elements[i]; i++) { if (!(el instanceof constr)) constr.decorate(el); } } /** * Defines a tracing UI component, a function that can be called to construct * the component. * * Base class: *
           * var List = ui.define('list');
           * List.prototype = {
           *   __proto__: HTMLUListElement.prototype,
           *   decorate: function() {
           *     ...
           *   },
           *   ...
           * };
           * 
        * * Derived class: *
           * var CustomList = ui.define('custom-list', List);
           * CustomList.prototype = {
           *   __proto__: List.prototype,
           *   decorate: function() {
           *     ...
           *   },
           *   ...
           * };
           * 
        * * @param {string} tagName The tagName of the newly created subtype. If * subclassing, this is used for debugging. If not subclassing, then it is * the tag name that will be created by the component. * @param {function=} opt_parentConstructor The parent class for this new * element, if subclassing is desired. If provided, the parent class must * be also a function created by ui.define. * @return {function(Object=):Element} The newly created component * constructor. */ function define(tagName, opt_parentConstructor) { if (typeof tagName == 'function') { throw new Error('Passing functions as tagName is deprecated. Please ' + 'use (tagName, opt_parentConstructor) to subclass'); } var tagName = tagName.toLowerCase(); if (opt_parentConstructor && !opt_parentConstructor.tagName) throw new Error('opt_parentConstructor was not created by ui.define'); /** * Creates a new UI element constructor. * Arguments passed to the constuctor are provided to the decorate method. * You will need to call the parent elements decorate method from within * your decorate method and pass any required parameters. * @constructor */ function f() { if (opt_parentConstructor && f.prototype.__proto__ != opt_parentConstructor.prototype) { throw new Error( tagName + ' prototye\'s __proto__ field is messed up. ' + 'It MUST be the prototype of ' + opt_parentConstructor.tagName); } // Walk up the parent constructors until we can find the type of tag // to create. var tag = tagName; if (opt_parentConstructor) { var parent = opt_parentConstructor; while (parent && parent.tagName) { tag = parent.tagName; parent = parent.parentConstructor; } } var el = base.doc.createElement(tag); f.decorate.call(this, el, arguments); return el; } try { // f.name is not directly writable. So make it writable anyway. Object.defineProperty( f, 'name', {value: tagName, writable: false, configurable: false}); } catch (e) { // defineProperty throws a TypeError about name already being defined // although, it also correctly sets the value to tagName. } /** * Decorates an element as a UI element class. * @param {!Element} el The element to decorate. */ f.decorate = function(el) { el.__proto__ = f.prototype; el.decorate.apply(el, arguments[1]); el.constructor = f; }; f.tagName = tagName; f.parentConstructor = (opt_parentConstructor ? opt_parentConstructor : undefined); f.toString = function() { if (!f.parentConstructor) return f.tagName; return f.parentConstructor.toString() + '::' + f.tagName; }; return f; } function elementIsChildOf(el, potentialParent) { if (el == potentialParent) return false; var cur = el; while (cur.parentNode) { if (cur == potentialParent) return true; cur = cur.parentNode; } return false; }; return { decorate: decorate, define: define, elementIsChildOf: elementIsChildOf }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Implements an element that is hidden by default, but * when shown, dims and (attempts to) disable the main document. * * You can turn any div into an overlay. Note that while an * overlay element is shown, its parent is changed. Hiding the overlay * restores its original parentage. * */ base.requireTemplate('ui.overlay'); base.require('base.utils'); base.require('base.properties'); base.require('base.events'); base.require('ui'); base.exportTo('ui', function() { /** * Creates a new overlay element. It will not be visible until shown. * @constructor * @extends {HTMLDivElement} */ var Overlay = ui.define('overlay'); Overlay.prototype = { __proto__: HTMLDivElement.prototype, /** * Initializes the overlay element. */ decorate: function() { this.classList.add('overlay'); this.parentEl_ = this.ownerDocument.body; this.visible_ = false; this.userCanClose_ = true; this.onKeyDown_ = this.onKeyDown_.bind(this); this.onClick_ = this.onClick_.bind(this); this.onFocusIn_ = this.onFocusIn_.bind(this); this.onDocumentClick_ = this.onDocumentClick_.bind(this); this.onClose_ = this.onClose_.bind(this); this.addEventListener('visibleChange', ui.Overlay.prototype.onVisibleChange_.bind(this), true); // Setup the shadow root this.shadow_ = this.webkitCreateShadowRoot(); this.shadow_.appendChild(base.instantiateTemplate('#overlay-template')); this.closeBtn_ = this.shadow_.querySelector('close-button'); this.closeBtn_.addEventListener('click', this.onClose_); this.shadow_ .querySelector('overlay-frame') .addEventListener('click', this.onClick_); this.observer_ = new WebKitMutationObserver( this.didButtonBarMutate_.bind(this)); this.observer_.observe(this.shadow_.querySelector('button-bar'), { childList: true }); // title is a variable on regular HTMLElements. However, we want to // use it for something more useful. Object.defineProperty( this, 'title', { get: function() { return this.shadow_.querySelector('title').textContent; }, set: function(title) { this.shadow_.querySelector('title').textContent = title; } }); }, set userCanClose(userCanClose) { this.userCanClose_ = userCanClose; this.closeBtn_.style.display = userCanClose ? 'block' : 'none'; }, get leftButtons() { return this.shadow_.querySelector('left-buttons'); }, get rightButtons() { return this.shadow_.querySelector('right-buttons'); }, get visible() { return this.visible_; }, set visible(newValue) { if (this.visible_ === newValue) return; base.setPropertyAndDispatchChange(this, 'visible', newValue); }, onVisibleChange_: function() { this.visible_ ? this.show_() : this.hide_(); }, show_: function() { this.parentEl_.appendChild(this); if (this.userCanClose_) { document.addEventListener('keydown', this.onKeyDown_); document.addEventListener('click', this.onDocumentClick_); } this.parentEl_.addEventListener('focusin', this.onFocusIn_); this.tabIndex = 0; // Focus the first thing we find that makes sense. (Skip the close button // as it doesn't make sense as the first thing to focus.) var focusEl = undefined; var elList = this.querySelectorAll('button, input, list, select, a'); if (elList.length > 0) { if (elList[0] === this.closeBtn_) { if (elList.length > 1) focusEl = elList[1]; } else { focusEl = elList[0]; } } if (focusEl === undefined) focusEl = this; focusEl.focus(); }, hide_: function() { this.parentEl_.removeChild(this); this.parentEl_.removeEventListener('focusin', this.onFocusIn_); if (this.closeBtn_) this.closeBtn_.removeEventListener(this.onClose_); document.removeEventListener('keydown', this.onKeyDown_); document.removeEventListener('click', this.onDocumentClick_); }, onClose_: function(e) { this.visible = false; e.stopPropagation(); e.preventDefault(); }, onFocusIn_: function(e) { if (e.target === this) return; window.setTimeout(function() { this.focus(); }, 0); e.preventDefault(); e.stopPropagation(); }, didButtonBarMutate_: function(e) { var hasButtons = this.leftButtons.children.length + this.rightButtons.children.length > 0; if (hasButtons) this.shadow_.querySelector('button-bar').style.display = undefined; else this.shadow_.querySelector('button-bar').style.display = 'none'; }, onKeyDown_: function(e) { // Disallow shift-tab back to another element. if (e.keyCode === 9 && // tab e.shiftKey && e.target === this) { e.preventDefault(); return; } if (e.keyCode !== 27) // escape return; this.visible = false; e.preventDefault(); }, onClick_: function(e) { e.stopPropagation(); }, onDocumentClick_: function(e) { if (!this.userCanClose_) return; this.visible = false; e.preventDefault(); e.stopPropagation(); } }; return { Overlay: Overlay }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview State and UI for trace data collection. */ base.requireStylesheet('about_tracing.tracing_controller'); base.require('base.properties'); base.require('base.events'); base.require('ui.overlay'); base.exportTo('about_tracing', function() { /** * The tracing controller is responsible for talking to tracing_ui.cc in * chrome * @constructor * @param {function(String, opt_Array.} Function to be used to send * data to chrome. */ function TracingController(sendFn) { this.sendFn_ = sendFn; this.overlay_ = new ui.Overlay(); this.overlay_.classList.add('recording-status-overlay'); this.statusDiv_ = document.createElement('div'); this.overlay_.appendChild(this.statusDiv_); this.bufferPercentDiv_ = document.createElement('div'); this.overlay_.appendChild(this.bufferPercentDiv_); this.stopButton_ = document.createElement('button'); this.stopButton_.onclick = this.endTracing.bind(this); this.stopButton_.textContent = 'Stop tracing'; this.overlay_.appendChild(this.stopButton_); this.traceEventData_ = undefined; this.systemTraceEvents_ = undefined; this.onKeydown_ = this.onKeydown_.bind(this); this.onKeypress_ = this.onKeypress_.bind(this); this.supportsSystemTracing_ = base.isChromeOS; if (this.sendFn_) this.sendFn_('tracingControllerInitialized'); } TracingController.prototype = { __proto__: base.EventTarget.prototype, gpuInfo_: undefined, clientInfo_: undefined, tracingEnabled_: false, tracingEnding_: false, systemTraceDataFilename_: undefined, get supportsSystemTracing() { return this.supportsSystemTracing_; }, onRequestBufferPercentFullComplete: function(percent_full) { if (!this.overlay_.visible) return; window.setTimeout(this.beginRequestBufferPercentFull_.bind(this), 500); var newText = 'Buffer usage: ' + Math.round(100 * percent_full) + '%'; if (this.bufferPercentDiv_.textContent != newText) this.bufferPercentDiv_.textContent = newText; }, /** * Begin requesting the buffer fullness */ beginRequestBufferPercentFull_: function() { this.sendFn_('beginRequestBufferPercentFull'); }, /** * Called by info_view to empty the trace buffer * * |opt_trace_categories| is a comma-delimited list of category wildcards. * A category can have an optional '-' prefix to make it an excluded * category. All the same rules apply above, so for example, having both * included and excluded categories in the same list would not be * supported. * * Example: beginTracing("test_MyTest*"); * Example: beginTracing("test_MyTest*,test_OtherStuff"); * Example: beginTracing("-excluded_category1,-excluded_category2"); */ beginTracing: function(opt_systemTracingEnabled, opt_trace_continuous, opt_enableSampling, opt_trace_categories) { if (this.tracingEnabled_) throw new Error('Tracing already begun.'); this.stopButton_.hidden = false; this.statusDiv_.textContent = 'Tracing active.'; this.overlay_.userCanClose = false; this.overlay_.visible = true; this.tracingEnabled_ = true; console.log('Beginning to trace...'); this.statusDiv_.textContent = 'Tracing active.'; var trace_options = []; trace_options.push(opt_trace_continuous ? 'record-continuously' : 'record-until-full'); if (opt_enableSampling) trace_options.push('enable-sampling'); this.traceEventData_ = undefined; this.systemTraceEvents_ = undefined; this.sendFn_( 'beginTracing', [ opt_systemTracingEnabled || false, opt_trace_categories || '-test_*', trace_options.join(',') ] ); this.beginRequestBufferPercentFull_(); window.addEventListener('keypress', this.onKeypress_); window.addEventListener('keydown', this.onKeydown_); }, onKeydown_: function(e) { if (e.keyCode == 27) { this.endTracing(); } }, onKeypress_: function(e) { if (e.keyIdentifier == 'Enter') { this.endTracing(); } }, /** * Called from gpu c++ code when ClientInfo is updated. */ onClientInfoUpdate: function(clientInfo) { this.clientInfo_ = clientInfo; }, /** * Called from gpu c++ code when GPU Info is updated. */ onGpuInfoUpdate: function(gpuInfo) { this.gpuInfo_ = gpuInfo; }, /** * Checks whether tracing is enabled */ get isTracingEnabled() { return this.tracingEnabled_; }, /** * Gets the currently traced events. If tracing is active, then * this can change on the fly. */ get traceEventData() { return this.traceEventData_; }, /** * Called to finish tracing and update all views. */ endTracing: function() { if (!this.tracingEnabled_) throw new Error('Tracing not begun.'); if (this.tracingEnding_) return; this.tracingEnding_ = true; this.statusDiv_.textContent = 'Ending trace...'; console.log('Finishing trace'); this.statusDiv_.textContent = 'Downloading trace data...'; this.stopButton_.hidden = true; // delay sending endTracingAsync until we get a chance to // update the screen... var that = this; window.setTimeout(function() { that.sendFn_('endTracingAsync'); }, 100); }, /** * Called by the browser when all processes complete tracing. */ onEndTracingComplete: function(traceDataString) { window.removeEventListener('keydown', this.onKeydown_); window.removeEventListener('keypress', this.onKeypress_); this.overlay_.visible = false; this.tracingEnabled_ = false; this.tracingEnding_ = false; if (traceDataString[traceDataString.length - 1] == ',') traceDataString = traceDataString.substr(0, traceDataString.length - 1); if (traceDataString[0] != '[') traceDataString = '[' + traceDataString; if (traceDataString[traceDataString.length - 1] != ']') traceDataString = traceDataString + ']'; this.traceEventData_ = traceDataString; console.log('onEndTracingComplete p1 with ' + this.traceEventData_.length + ' bytes of data.'); var e = new base.Event('traceEnded'); this.dispatchEvent(e); }, collectCategories: function() { this.sendFn_('getKnownCategories'); }, onKnownCategoriesCollected: function(categories) { var e = new base.Event('categoriesCollected'); e.categories = categories; this.dispatchEvent(e); }, /** * Called by tracing c++ code when new system trace data arrives. */ onSystemTraceDataCollected: function(events) { console.log('onSystemTraceDataCollected with ' + events.length + ' chars of data.'); this.systemTraceEvents_ = events; }, /** * Gets the currentl system trace events. If tracing is active, then * this can change on the fly. */ get systemTraceEvents() { return this.systemTraceEvents_; }, /** * Tells browser to put up a load dialog and load the trace file */ beginLoadTraceFile: function() { this.sendFn_('loadTraceFile'); }, /** * Called by the browser when a trace file is loaded. */ onLoadTraceFileComplete: function(traceDataString, opt_filename) { this.traceEventData_ = traceDataString; this.systemTraceEvents_ = undefined; var e = new base.Event('loadTraceFileComplete'); e.filename = opt_filename || ''; this.dispatchEvent(e); }, /** * Called by the browser when loading a trace file was canceled. */ onLoadTraceFileCanceled: function() { base.dispatchSimpleEvent(this, 'loadTraceFileCanceled'); }, /** * Tells browser to put up a save dialog and save the trace file */ beginSaveTraceFile: function() { // this.traceEventData_ is already in JSON form, but now need to insert it // into a data structure containing metadata about the recording. To do // this "right," we should parse the traceEventData_, make the new data // structure and then JSONize the lot. But, the traceEventData_ is huge so // parsing it and stringifying it again is going to consume time and // memory. // // Instead, we make the new data strcture with a placeholder string, // JSONify it, then replace the placeholder string with the // traceEventData_. var data = { traceEvents: '__TRACE_EVENT_PLACEHOLDER__', systemTraceEvents: this.systemTraceEvents_, clientInfo: this.clientInfo_, gpuInfo: this.gpuInfo_ }; var dataAsString = JSON.stringify(data); dataAsString = dataAsString.replace('"__TRACE_EVENT_PLACEHOLDER__"', this.traceEventData_); this.sendFn_('saveTraceFile', [dataAsString]); }, /** * Called by the browser when a trace file is saveed. */ onSaveTraceFileComplete: function() { base.dispatchSimpleEvent(this, 'saveTraceFileComplete'); }, /** * Called by the browser when saving a trace file was canceled. */ onSaveTraceFileCanceled: function() { base.dispatchSimpleEvent(this, 'saveTraceFileCanceled'); } }; return { TracingController: TracingController }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview 2D bounding box computations. */ base.require('base.gl_matrix'); base.require('base.rect'); base.exportTo('base', function() { /** * Tracks a 2D bounding box. * @constructor */ function BBox2() { this.isEmpty_ = true; this.min_ = undefined; this.max_ = undefined; }; BBox2.prototype = { __proto__: Object.prototype, reset: function() { this.isEmpty_ = true; this.min_ = undefined; this.max_ = undefined; }, get isEmpty() { return this.isEmpty_; }, addBBox2: function(bbox2) { if (bbox2.isEmpty) return; this.addVec2(bbox2.min_); this.addVec2(bbox2.max_); }, clone: function() { var bbox = new BBox2(); bbox.addBBox2(this); return bbox; }, /** * Adds x, y to the range. */ addXY: function(x, y) { if (this.isEmpty_) { this.max_ = vec2.create(); this.min_ = vec2.create(); vec2.set(this.max_, x, y); vec2.set(this.min_, x, y); this.isEmpty_ = false; return; } this.max_[0] = Math.max(this.max_[0], x); this.max_[1] = Math.max(this.max_[1], y); this.min_[0] = Math.min(this.min_[0], x); this.min_[1] = Math.min(this.min_[1], y); }, /** * Adds value_x, value_y in the form [value_x,value_y] to the range. */ addVec2: function(value) { if (this.isEmpty_) { this.max_ = vec2.create(); this.min_ = vec2.create(); vec2.set(this.max_, value[0], value[1]); vec2.set(this.min_, value[0], value[1]); this.isEmpty_ = false; return; } this.max_[0] = Math.max(this.max_[0], value[0]); this.max_[1] = Math.max(this.max_[1], value[1]); this.min_[0] = Math.min(this.min_[0], value[0]); this.min_[1] = Math.min(this.min_[1], value[1]); }, addQuad: function(quad) { this.addVec2(quad.p1); this.addVec2(quad.p2); this.addVec2(quad.p3); this.addVec2(quad.p4); }, get minVec2() { if (this.isEmpty_) return undefined; return this.min_; }, get maxVec2() { if (this.isEmpty_) return undefined; return this.max_; }, get sizeAsVec2() { if (this.isEmpty_) throw new Error('Empty BBox2 has no size'); var size = vec2.create(); vec2.subtract(size, this.max_, this.min_); return size; }, get size() { if (this.isEmpty_) throw new Error('Empty BBox2 has no size'); return {width: this.max_[0] - this.min_[0], height: this.max_[1] - this.min_[1]}; }, get width() { if (this.isEmpty_) throw new Error('Empty BBox2 has no width'); return this.max_[0] - this.min_[0]; }, get height() { if (this.isEmpty_) throw new Error('Empty BBox2 has no width'); return this.max_[1] - this.min_[1]; }, toString: function() { if (this.isEmpty_) return 'empty'; return 'min=(' + this.min_[0] + ',' + this.min_[1] + ') ' + 'max=(' + this.max_[0] + ',' + this.max_[1] + ')'; }, asRect: function() { return base.Rect.fromXYWH( this.min_[0], this.min_[1], this.max_[0] - this.min_[0], this.max_[1] - this.min_[1]); } }; return { BBox2: BBox2 }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.exportTo('cc', function() { var constants = {}; constants.ACTIVE_TREE = 0; constants.PENDING_TREE = 1; constants.HIGH_PRIORITY_BIN = 0; constants.LOW_PRIORITY_BIN = 1; return { constants: constants }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.rect'); base.exportTo('cc', function() { /** * @constructor */ function Region() { this.rects = []; } Region.fromArray = function(array) { if (array.length % 4 != 0) throw new Error('Array must consist be a multiple of 4 in length'); var r = new Region(); for (var i = 0; i < array.length; i += 4) { r.rects.push(base.Rect.fromXYWH(array[i], array[i + 1], array[i + 2], array[i + 3])); } return r; } /** * @return {Region} If array is undefined, returns an empty region. Otherwise * returns Region.fromArray(array). */ Region.fromArrayOrUndefined = function(array) { if (array === undefined) return new Region(); return Region.fromArray(array); }; Region.prototype = { __proto__: Region.prototype, rectIntersects: function(r) { for (var i = 0; i < this.rects.length; i++) { if (this.rects[i].intersects(r)) return true; } return false; } }; return { Region: Region }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.exportTo('cc', function() { /** * This class represents a tile (from impl side) and its final rect on the * layer. Note that the rect is determined by what is needed to cover all * of the layer without overlap. * @constructor */ function TileCoverageRect(rect, tile) { this.geometryRect = rect; this.tile = tile; } return { TileCoverageRect: TileCoverageRect }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Quick range computations. */ base.exportTo('base', function() { function Range() { this.isEmpty_ = true; this.min_ = undefined; this.max_ = undefined; }; Range.prototype = { __proto__: Object.prototype, reset: function() { this.isEmpty_ = true; this.min_ = undefined; this.max_ = undefined; }, get isEmpty() { return this.isEmpty_; }, addRange: function(range) { if (range.isEmpty) return; this.addValue(range.min); this.addValue(range.max); }, addValue: function(value) { if (this.isEmpty_) { this.max_ = value; this.min_ = value; this.isEmpty_ = false; return; } this.max_ = Math.max(this.max_, value); this.min_ = Math.min(this.min_, value); }, get min() { if (this.isEmpty_) return undefined; return this.min_; }, get max() { if (this.isEmpty_) return undefined; return this.max_; }, get range() { if (this.isEmpty_) return undefined; return this.max_ - this.min_; }, get center() { return (this.min_ + this.max_) * 0.5; } }; Range.compareByMinTimes = function(a, b) { if (!a.isEmpty && !b.isEmpty) return a.min_ - b.min_; if (a.isEmpty && !b.isEmpty) return -1; if (!a.isEmpty && b.isEmpty) return 1; return 0; }; return { Range: Range }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Helper functions for doing intersections and iteration * over sorted arrays and intervals. * */ base.exportTo('base', function() { /** * Finds the first index in the array whose value is >= loVal. * * The key for the search is defined by the mapFn. This array must * be prearranged such that ary.map(mapFn) would also be sorted in * ascending order. * * @param {Array} ary An array of arbitrary objects. * @param {function():*} mapFn Callback that produces a key value * from an element in ary. * @param {number} loVal Value for which to search. * @return {Number} Offset o into ary where all ary[i] for i <= o * are < loVal, or ary.length if loVal is greater than all elements in * the array. */ function findLowIndexInSortedArray(ary, mapFn, loVal) { if (ary.length == 0) return 1; var low = 0; var high = ary.length - 1; var i, comparison; var hitPos = -1; while (low <= high) { i = Math.floor((low + high) / 2); comparison = mapFn(ary[i]) - loVal; if (comparison < 0) { low = i + 1; continue; } else if (comparison > 0) { high = i - 1; continue; } else { hitPos = i; high = i - 1; } } // return where we hit, or failing that the low pos return hitPos != -1 ? hitPos : low; } /** * Finds an index in an array of intervals that either * intersects the provided loVal, or if no intersection is found, * the index of the first interval whose start is > loVal. * * The array of intervals is defined implicitly via two mapping functions * over the provided ary. mapLoFn determines the lower value of the interval, * mapWidthFn the width. Intersection is lower-inclusive, e.g. [lo,lo+w). * * The array of intervals formed by this mapping must be non-overlapping and * sorted in ascending order by loVal. * * @param {Array} ary An array of objects that can be converted into sorted * nonoverlapping ranges [x,y) using the mapLoFn and mapWidth. * @param {function():*} mapLoFn Callback that produces the low value for the * interval represented by an element in the array. * @param {function():*} mapWidthFn Callback that produces the width for the * interval represented by an element in the array. * @param {number} loVal The low value for the search. * @return {Number} An index in the array that intersects or is first-above * loVal, -1 if none found and loVal is below than all the intervals, * ary.length if loVal is greater than all the intervals. */ function findLowIndexInSortedIntervals(ary, mapLoFn, mapWidthFn, loVal) { var first = findLowIndexInSortedArray(ary, mapLoFn, loVal); if (first == 0) { if (loVal >= mapLoFn(ary[0]) && loVal < mapLoFn(ary[0]) + mapWidthFn(ary[0], 0)) { return 0; } else { return -1; } } else if (first < ary.length) { if (loVal >= mapLoFn(ary[first]) && loVal < mapLoFn(ary[first]) + mapWidthFn(ary[first], first)) { return first; } else if (loVal >= mapLoFn(ary[first - 1]) && loVal < mapLoFn(ary[first - 1]) + mapWidthFn(ary[first - 1], first - 1)) { return first - 1; } else { return ary.length; } } else if (first == ary.length) { if (loVal >= mapLoFn(ary[first - 1]) && loVal < mapLoFn(ary[first - 1]) + mapWidthFn(ary[first - 1], first - 1)) { return first - 1; } else { return ary.length; } } else { return ary.length; } } /** * Calls cb for all intervals in the implicit array of intervals * defnied by ary, mapLoFn and mapHiFn that intersect the range * [loVal,hiVal) * * This function uses the same scheme as findLowIndexInSortedArray * to define the intervals. The same restrictions on sortedness and * non-overlappingness apply. * * @param {Array} ary An array of objects that can be converted into sorted * nonoverlapping ranges [x,y) using the mapLoFn and mapWidth. * @param {function():*} mapLoFn Callback that produces the low value for the * interval represented by an element in the array. * @param {function():*} mapLoFn Callback that produces the width for the * interval represented by an element in the array. * @param {number} The low value for the search, inclusive. * @param {number} loVal The high value for the search, non inclusive. * @param {function():*} cb The function to run for intersecting intervals. */ function iterateOverIntersectingIntervals(ary, mapLoFn, mapWidthFn, loVal, hiVal, cb) { if (ary.length == 0) return; if (loVal > hiVal) return; var i = findLowIndexInSortedArray(ary, mapLoFn, loVal); if (i == -1) { return; } if (i > 0) { var hi = mapLoFn(ary[i - 1]) + mapWidthFn(ary[i - 1], i - 1); if (hi >= loVal) { cb(ary[i - 1]); } } if (i == ary.length) { return; } for (var n = ary.length; i < n; i++) { var lo = mapLoFn(ary[i]); if (lo >= hiVal) break; cb(ary[i]); } } /** * Non iterative version of iterateOverIntersectingIntervals. * * @return {Array} Array of elements in ary that intersect loVal, hiVal. */ function getIntersectingIntervals(ary, mapLoFn, mapWidthFn, loVal, hiVal) { var tmp = []; iterateOverIntersectingIntervals(ary, mapLoFn, mapWidthFn, loVal, hiVal, function(d) { tmp.push(d); }); return tmp; } /** * Finds the element in the array whose value is closest to |val|. * * The same restrictions on sortedness as for findLowIndexInSortedArray apply. * * @param {Array} ary An array of arbitrary objects. * @param {function():*} mapFn Callback that produces a key value * from an element in ary. * @param {number} val Value for which to search. * @param {number} maxDiff Maximum allowed difference in value between |val| * and an element's value. * @return {object} Object in the array whose value is closest to |val|, or * null if no object is within range. */ function findClosestElementInSortedArray(ary, mapFn, val, maxDiff) { if (ary.length === 0) return null; var aftIdx = findLowIndexInSortedArray(ary, mapFn, val); var befIdx = aftIdx > 0 ? aftIdx - 1 : 0; if (aftIdx === ary.length) aftIdx -= 1; var befDiff = Math.abs(val - mapFn(ary[befIdx])); var aftDiff = Math.abs(val - mapFn(ary[aftIdx])); if (befDiff > maxDiff && aftDiff > maxDiff) return null; var idx = befDiff < aftDiff ? befIdx : aftIdx; return ary[idx]; } /** * Finds the closest interval in the implicit array of intervals * defined by ary, mapLoFn and mapHiFn. * * This function uses the same scheme as findLowIndexInSortedArray * to define the intervals. The same restrictions on sortedness and * non-overlappingness apply. * * @param {Array} ary An array of objects that can be converted into sorted * nonoverlapping ranges [x,y) using the mapLoFn and mapHiFn. * @param {function():*} mapLoFn Callback that produces the low value for the * interval represented by an element in the array. * @param {function():*} mapHiFn Callback that produces the high for the * interval represented by an element in the array. * @param {number} val The value for the search. * @param {number} maxDiff Maximum allowed difference in value between |val| * and an interval's low or high value. * @return {interval} Interval in the array whose high or low value is closest * to |val|, or null if no interval is within range. */ function findClosestIntervalInSortedIntervals(ary, mapLoFn, mapHiFn, val, maxDiff) { if (ary.length === 0) return null; var idx = findLowIndexInSortedArray(ary, mapLoFn, val); if (idx > 0) idx -= 1; var hiInt = ary[idx]; var loInt = hiInt; if (val > mapHiFn(hiInt) && idx + 1 < ary.length) loInt = ary[idx + 1]; var loDiff = Math.abs(val - mapLoFn(loInt)); var hiDiff = Math.abs(val - mapHiFn(hiInt)); if (loDiff > maxDiff && hiDiff > maxDiff) return null; if (loDiff < hiDiff) return loInt; else return hiInt; } return { findLowIndexInSortedArray: findLowIndexInSortedArray, findLowIndexInSortedIntervals: findLowIndexInSortedIntervals, iterateOverIntersectingIntervals: iterateOverIntersectingIntervals, getIntersectingIntervals: getIntersectingIntervals, findClosestElementInSortedArray: findClosestElementInSortedArray, findClosestIntervalInSortedIntervals: findClosestIntervalInSortedIntervals }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.exportTo('base', function() { var nextGUID = 1; var GUID = { allocate: function() { return nextGUID++; } }; return { GUID: GUID }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.guid'); /** * @fileoverview Provides the Event class. */ base.exportTo('tracing.trace_model', function() { /** * The SelectionState enum defines how Events are displayed in the view. */ var SelectionState = { NONE: 0, SELECTED: 1, HIGHLIGHTED: 2, DIMMED: 3 }; /** * A Event is the base type for any non-container, selectable piece * of data in the trace model. * * @constructor */ function Event() { this.guid_ = base.GUID.allocate(); this.selectionState = SelectionState.NONE; } Event.prototype = { get guid() { return this.guid_; }, get selected() { return this.selectionState === SelectionState.SELECTED; } }; return { Event: Event, SelectionState: SelectionState }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.trace_model.event'); base.exportTo('tracing.trace_model', function() { /** * A snapshot of an object instance, at a given moment in time. * * Initialization of snapshots and instances is three phased: * * 1. Instances and snapshots are constructed. This happens during event * importing. Little should be done here, because the object's data * are still being used by the importer to reconstruct object references. * * 2. Instances and snapshtos are preinitialized. This happens after implicit * objects have been found, but before any references have been found and * switched to direct references. Thus, every snapshot stands on its own. * This is a good time to do global field renaming and type conversion, * e.g. recognizing domain-specific types and converting from C++ naming * convention to JS. * * 3. Instances and snapshtos are initialized. At this point, {id_ref: * '0x1000'} fields have been converted to snapshot references. This is a * good time to generic initialization steps and argument verification. * * @constructor */ function ObjectSnapshot(objectInstance, ts, args) { tracing.trace_model.Event.call(this); this.objectInstance = objectInstance; this.ts = ts; this.args = args; } ObjectSnapshot.prototype = { __proto__: tracing.trace_model.Event.prototype, /** * See ObjectSnapshot constructor notes on object initialization. */ preInitialize: function() { }, /** * See ObjectSnapshot constructor notes on object initialization. */ initialize: function() { }, addBoundsToRange: function(range) { range.addValue(this.ts); } }; ObjectSnapshot.nameToConstructorMap_ = {}; ObjectSnapshot.register = function(name, constructor) { if (ObjectSnapshot.nameToConstructorMap_[name]) throw new Error('Constructor already registerd for ' + name); ObjectSnapshot.nameToConstructorMap_[name] = constructor; }; ObjectSnapshot.unregister = function(name) { delete ObjectSnapshot.nameToConstructorMap_[name]; }; ObjectSnapshot.getConstructor = function(name) { if (ObjectSnapshot.nameToConstructorMap_[name]) return ObjectSnapshot.nameToConstructorMap_[name]; return ObjectSnapshot; }; return { ObjectSnapshot: ObjectSnapshot }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the ObjectSnapshot and ObjectHistory classes. */ base.require('base.range'); base.require('base.sorted_array_utils'); base.require('tracing.trace_model.event'); base.require('tracing.trace_model.object_snapshot'); base.exportTo('tracing.trace_model', function() { var ObjectSnapshot = tracing.trace_model.ObjectSnapshot; /** * An object with a specific id, whose state has been snapshotted several * times. * * @constructor */ function ObjectInstance(parent, id, category, name, creationTs) { tracing.trace_model.Event.call(this); this.parent = parent; this.id = id; this.category = category; this.name = name; this.creationTs = creationTs; this.creationTsWasExplicit = false; this.deletionTs = Number.MAX_VALUE; this.deletionTsWasExplicit = false; this.colorId = 0; this.bounds = new base.Range(); this.snapshots = []; this.hasImplicitSnapshots = false; } ObjectInstance.prototype = { __proto__: tracing.trace_model.Event.prototype, get typeName() { return this.name; }, addBoundsToRange: function(range) { range.addRange(this.bounds); }, addSnapshot: function(ts, args) { if (ts < this.creationTs) throw new Error('Snapshots must be >= instance.creationTs'); if (ts >= this.deletionTs) throw new Error('Snapshots cannot be added after ' + 'an objects deletion timestamp.'); var lastSnapshot; if (this.snapshots.length > 0) { lastSnapshot = this.snapshots[this.snapshots.length - 1]; if (lastSnapshot.ts == ts) throw new Error('Snapshots already exists at this time!'); if (ts < lastSnapshot.ts) { throw new Error( 'Snapshots must be added in increasing timestamp order'); } } var snapshotConstructor = tracing.trace_model.ObjectSnapshot.getConstructor(this.name); var snapshot = new snapshotConstructor(this, ts, args); this.snapshots.push(snapshot); return snapshot; }, wasDeleted: function(ts) { var lastSnapshot; if (this.snapshots.length > 0) { lastSnapshot = this.snapshots[this.snapshots.length - 1]; if (lastSnapshot.ts > ts) throw new Error( 'Instance cannot be deleted at ts=' + ts + '. A snapshot exists that is older.'); } this.deletionTs = ts; this.deletionTsWasExplicit = true; }, /** * See ObjectSnapshot constructor notes on object initialization. */ preInitialize: function() { for (var i = 0; i < this.snapshots.length; i++) this.snapshots[i].preInitialize(); }, /** * See ObjectSnapshot constructor notes on object initialization. */ initialize: function() { for (var i = 0; i < this.snapshots.length; i++) this.snapshots[i].initialize(); }, getSnapshotAt: function(ts) { if (ts < this.creationTs) { if (this.creationTsWasExplicit) throw new Error('ts must be within lifetime of this instance'); return this.snapshots[0]; } if (ts > this.deletionTs) throw new Error('ts must be within lifetime of this instance'); var snapshots = this.snapshots; var i = base.findLowIndexInSortedIntervals( snapshots, function(snapshot) { return snapshot.ts; }, function(snapshot, i) { if (i == snapshots.length - 1) return snapshots[i].objectInstance.deletionTs; return snapshots[i + 1].ts - snapshots[i].ts; }, ts); if (i < 0) { // Note, this is a little bit sketchy: this lets early ts point at the // first snapshot, even before it is taken. We do this because raster // tasks usually post before their tile snapshots are dumped. This may // be a good line of code to re-visit if we start seeing strange and // confusing object references showing up in the traces. return this.snapshots[0]; } if (i >= this.snapshots.length) return this.snapshots[this.snapshots.length - 1]; return this.snapshots[i]; }, updateBounds: function() { this.bounds.reset(); this.bounds.addValue(this.creationTs); if (this.deletionTs != Number.MAX_VALUE) this.bounds.addValue(this.deletionTs); else if (this.snapshots.length > 0) this.bounds.addValue(this.snapshots[this.snapshots.length - 1].ts); }, shiftTimestampsForward: function(amount) { this.creationTs += amount; if (this.deletionTs != Number.MAX_VALUE) this.deletionTs += amount; this.snapshots.forEach(function(snapshot) { snapshot.ts += amount; }); } }; ObjectInstance.nameToConstructorMap_ = {}; ObjectInstance.register = function(name, constructor) { if (ObjectInstance.nameToConstructorMap_[name]) throw new Error('Constructor already registerd for ' + name); ObjectInstance.nameToConstructorMap_[name] = constructor; }; ObjectInstance.unregister = function(name) { delete ObjectInstance.nameToConstructorMap_[name]; }; ObjectInstance.getConstructor = function(name) { if (ObjectInstance.nameToConstructorMap_[name]) return ObjectInstance.nameToConstructorMap_[name]; return ObjectInstance; }; return { ObjectInstance: ObjectInstance }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.rect'); base.require('cc.constants'); base.require('cc.region'); base.require('cc.tile_coverage_rect'); base.require('tracing.trace_model.object_instance'); base.exportTo('cc', function() { var constants = cc.constants; var ObjectSnapshot = tracing.trace_model.ObjectSnapshot; /** * @constructor */ function LayerImplSnapshot() { ObjectSnapshot.apply(this, arguments); } LayerImplSnapshot.prototype = { __proto__: ObjectSnapshot.prototype, preInitialize: function() { cc.preInitializeObject(this); this.layerTreeImpl_ = undefined; this.parentLayer = undefined; }, initialize: function() { // Defaults. this.invalidation = new cc.Region(); this.pictures = []; // Import & validate this.args cc.moveRequiredFieldsFromArgsToToplevel( this, ['layerId', 'children', 'layerQuad']); cc.moveOptionalFieldsFromArgsToToplevel( this, ['maskLayer', 'replicaLayer', 'idealContentsScale', 'geometryContentsScale']); // Leave bounds in both places. this.bounds = base.Rect.fromXYWH( 0, 0, this.args.bounds.width, this.args.bounds.height); for (var i = 0; i < this.children.length; i++) this.children[i].parentLayer = this; if (this.maskLayer) this.maskLayer.parentLayer = this; if (this.replicaLayer) this.maskLayer.replicaLayer = this; if (!this.geometryContentsScale) this.geometryContentsScale = 1.0; this.touchEventHandlerRegion = cc.Region.fromArrayOrUndefined( this.args.touchEventHandlerRegion); this.wheelEventHandlerRegion = cc.Region.fromArrayOrUndefined( this.args.wheelEventHandlerRegion); this.nonFastScrollableRegion = cc.Region.fromArrayOrUndefined( this.args.nonFastScrollableRegion); }, get layerTreeImpl() { if (this.layerTreeImpl_) return this.layerTreeImpl_; if (this.parentLayer) return this.parentLayer.layerTreeImpl; return undefined; }, set layerTreeImpl(layerTreeImpl) { this.layerTreeImpl_ = layerTreeImpl; }, get activeLayer() { if (this.layerTreeImpl.whichTree == constants.ACTIVE_TREE) return this; var activeTree = this.layerTreeImpl.layerTreeHostImpl.activeTree; return activeTree.findLayerWithId(this.layerId); }, get pendingLayer() { if (this.layerTreeImpl.whichTree == constants.PENDING_TREE) return this; var pendingTree = this.layerTreeImpl.layerTreeHostImpl.pendingTree; return pendingTree.findLayerWithId(this.layerId); } }; /** * @constructor */ function PictureLayerImplSnapshot() { LayerImplSnapshot.apply(this, arguments); } PictureLayerImplSnapshot.prototype = { __proto__: LayerImplSnapshot.prototype, initialize: function() { LayerImplSnapshot.prototype.initialize.call(this); if (this.args.invalidation) { this.invalidation = cc.Region.fromArray(this.args.invalidation); delete this.args.invalidation; } if (this.args.pictures) { this.pictures = this.args.pictures; } this.tileCoverageRects = []; if (this.args.coverageTiles) { for (var i = 0; i < this.args.coverageTiles.length; ++i) { var rect = this.args.coverageTiles[i].geometryRect; var tile = this.args.coverageTiles[i].tile; this.tileCoverageRects.push(new cc.TileCoverageRect(rect, tile)); } delete this.args.coverageTiles; } } }; ObjectSnapshot.register('cc::LayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::PictureLayerImpl', PictureLayerImplSnapshot); ObjectSnapshot.register('cc::DelegatedRendererLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::HeadsUpDisplayLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::IOSurfaceLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::NinePatchLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::PictureImageLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::ScrollbarLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::SolidColorLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::TextureLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::TiledLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::VideoLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('cc::PaintedScrollbarLayerImpl', LayerImplSnapshot); ObjectSnapshot.register('ClankPatchLayer', LayerImplSnapshot); ObjectSnapshot.register('TabBorderLayer', LayerImplSnapshot); ObjectSnapshot.register('CounterLayer', LayerImplSnapshot); return { LayerImplSnapshot: LayerImplSnapshot, PictureLayerImplSnapshot: PictureLayerImplSnapshot }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.trace_model.object_instance'); base.require('cc.layer_impl'); base.exportTo('cc', function() { var ObjectSnapshot = tracing.trace_model.ObjectSnapshot; /** * @constructor */ function LayerTreeImplSnapshot() { ObjectSnapshot.apply(this, arguments); } LayerTreeImplSnapshot.prototype = { __proto__: ObjectSnapshot.prototype, preInitialize: function() { cc.preInitializeObject(this); this.layerTreeHostImpl = undefined; this.whichTree = undefined; }, initialize: function() { cc.moveRequiredFieldsFromArgsToToplevel( this, ['rootLayer', 'renderSurfaceLayerList']); this.rootLayer.layerTreeImpl = this; }, iterLayers: function(func, thisArg) { var visitedLayers = {}; function visitLayer(layer, depth, isMask, isReplica) { if (visitedLayers[layer.layerId]) return; visitedLayers[layer.layerId] = true; func.call(thisArg, layer, depth, isMask, isReplica); for (var i = 0; i < layer.children.length; i++) visitLayer(layer.children[i], depth + 1); if (layer.maskLayer) visitLayer(layer, depth + 1, true, false); if (layer.replicaLayer) visitLayer(layer, depth + 1, false, true); } visitLayer(this.rootLayer, 0, false, false); }, findLayerWithId: function(id) { var foundLayer = undefined; function visitLayer(layer) { if (layer.layerId == id) foundLayer = layer; } this.iterLayers(visitLayer); return foundLayer; } }; ObjectSnapshot.register('cc::LayerTreeImpl', LayerTreeImplSnapshot); return { LayerTreeImplSnapshot: LayerTreeImplSnapshot }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.gl_matrix'); base.exportTo('base', function() { var tmpVec2s = []; for (var i = 0; i < 8; i++) tmpVec2s[i] = vec2.create(); var tmpVec2a = vec4.create(); var tmpVec4a = vec4.create(); var tmpVec4b = vec4.create(); var tmpMat4 = mat4.create(); var tmpMat4b = mat4.create(); var p00 = vec2.createXY(0, 0); var p10 = vec2.createXY(1, 0); var p01 = vec2.createXY(0, 1); var p11 = vec2.createXY(1, 1); var lerpingVecA = vec2.create(); var lerpingVecB = vec2.create(); function lerpVec2(out, a, b, amt) { vec2.scale(lerpingVecA, a, amt); vec2.scale(lerpingVecB, b, 1 - amt); vec2.add(out, lerpingVecA, lerpingVecB); vec2.normalize(out, out); return out; } /** * @constructor */ function Quad() { this.p1 = vec2.create(); this.p2 = vec2.create(); this.p3 = vec2.create(); this.p4 = vec2.create(); } Quad.fromXYWH = function(x, y, w, h) { var q = new Quad(); vec2.set(q.p1, x, y); vec2.set(q.p2, x + w, y); vec2.set(q.p3, x + w, y + h); vec2.set(q.p4, x, y + h); return q; } Quad.fromRect = function(r) { return new Quad.fromXYWH( r.x, r.y, r.width, r.height); } Quad.from4Vecs = function(p1, p2, p3, p4) { var q = new Quad(); vec2.set(q.p1, p1[0], p1[1]); vec2.set(q.p2, p2[0], p2[1]); vec2.set(q.p3, p3[0], p3[1]); vec2.set(q.p4, p4[0], p4[1]); return q; } Quad.from8Array = function(arr) { if (arr.length != 8) throw new Error('Array must be 8 long'); var q = new Quad(); q.p1[0] = arr[0]; q.p1[1] = arr[1]; q.p2[0] = arr[2]; q.p2[1] = arr[3]; q.p3[0] = arr[4]; q.p3[1] = arr[5]; q.p4[0] = arr[6]; q.p4[1] = arr[7]; return q; }; Quad.prototype = { pointInside: function(point) { return pointInImplicitQuad(point, this.p1, this.p2, this.p3, this.p4); }, boundingRect: function() { var x0 = Math.min(this.p1[0], this.p2[0], this.p3[0], this.p4[0]); var y0 = Math.min(this.p1[1], this.p2[1], this.p3[1], this.p4[1]); var x1 = Math.max(this.p1[0], this.p2[0], this.p3[0], this.p4[0]); var y1 = Math.max(this.p1[1], this.p2[1], this.p3[1], this.p4[1]); return new base.Rect.fromXYWH(x0, y0, x1 - x0, y1 - y0); }, clone: function() { var q = new Quad(); vec2.copy(q.p1, this.p1); vec2.copy(q.p2, this.p2); vec2.copy(q.p3, this.p3); vec2.copy(q.p4, this.p4); return q; }, scale: function(s) { var q = new Quad(); this.scaleFast(q, s); return q; }, scaleFast: function(dstQuad, s) { vec2.copy(dstQuad.p1, this.p1, s); vec2.copy(dstQuad.p2, this.p2, s); vec2.copy(dstQuad.p3, this.p3, s); vec2.copy(dstQuad.p3, this.p3, s); }, isRectangle: function() { // Simple rectangle check. Note: will not handle out-of-order components. var bounds = this.boundingRect(); return ( bounds.x == this.p1[0] && bounds.y == this.p1[1] && bounds.width == this.p2[0] - this.p1[0] && bounds.y == this.p2[1] && bounds.width == this.p3[0] - this.p1[0] && bounds.height == this.p3[1] - this.p2[1] && bounds.x == this.p4[0] && bounds.height == this.p4[1] - this.p2[1] ); }, projectUnitRect: function(rect) { var q = new Quad(); this.projectUnitRectFast(q, rect); return q; }, projectUnitRectFast: function(dstQuad, rect) { var v12 = tmpVec2s[0]; var v14 = tmpVec2s[1]; var v23 = tmpVec2s[2]; var v43 = tmpVec2s[3]; var l12, l14, l23, l43; vec2.sub(v12, this.p2, this.p1); l12 = vec2.length(v12); vec2.scale(v12, v12, 1 / l12); vec2.sub(v14, this.p4, this.p1); l14 = vec2.length(v14); vec2.scale(v14, v14, 1 / l14); vec2.sub(v23, this.p3, this.p2); l23 = vec2.length(v23); vec2.scale(v23, v23, 1 / l23); vec2.sub(v43, this.p3, this.p4); l43 = vec2.length(v43); vec2.scale(v43, v43, 1 / l43); var b12 = tmpVec2s[0]; var b14 = tmpVec2s[1]; var b23 = tmpVec2s[2]; var b43 = tmpVec2s[3]; lerpVec2(b12, v12, v43, rect.y); lerpVec2(b43, v12, v43, 1 - rect.bottom); lerpVec2(b14, v14, v23, rect.x); lerpVec2(b23, v14, v23, 1 - rect.right); vec2.addTwoScaledUnitVectors(tmpVec2a, b12, l12 * rect.x, b14, l14 * rect.y); vec2.add(dstQuad.p1, this.p1, tmpVec2a); vec2.addTwoScaledUnitVectors(tmpVec2a, b12, l12 * -(1.0 - rect.right), b23, l23 * rect.y); vec2.add(dstQuad.p2, this.p2, tmpVec2a); vec2.addTwoScaledUnitVectors(tmpVec2a, b43, l43 * -(1.0 - rect.right), b23, l23 * -(1.0 - rect.bottom)); vec2.add(dstQuad.p3, this.p3, tmpVec2a); vec2.addTwoScaledUnitVectors(tmpVec2a, b43, l43 * rect.left, b14, l14 * -(1.0 - rect.bottom)); vec2.add(dstQuad.p4, this.p4, tmpVec2a); }, toString: function() { return 'Quad(' + vec2.toString(this.p1) + ', ' + vec2.toString(this.p2) + ', ' + vec2.toString(this.p3) + ', ' + vec2.toString(this.p4) + ')'; } }; function sign(p1, p2, p3) { return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]); } function pointInTriangle2(pt, p1, p2, p3) { var b1 = sign(pt, p1, p2) < 0.0; var b2 = sign(pt, p2, p3) < 0.0; var b3 = sign(pt, p3, p1) < 0.0; return ((b1 == b2) && (b2 == b3)); } function pointInImplicitQuad(point, p1, p2, p3, p4) { return pointInTriangle2(point, p1, p2, p3) || pointInTriangle2(point, p1, p3, p4); } return { pointInTriangle2: pointInTriangle2, pointInImplicitQuad: pointInImplicitQuad, Quad: Quad }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.quad'); base.require('base.rect'); base.require('tracing.trace_model.object_instance'); base.exportTo('cc', function() { var convertedNameCache = {}; function convertNameToJSConvention(name) { if (name in convertedNameCache) return convertedNameCache[name]; if (name[0] == '_' || name[name.length - 1] == '_') { convertedNameCache[name] = name; return name; } var words = name.split('_'); if (words.length == 1) { convertedNameCache[name] = words[0]; return words[0]; } for (var i = 1; i < words.length; i++) words[i] = words[i][0].toUpperCase() + words[i].substring(1); convertedNameCache[name] = words.join(''); return convertedNameCache[name]; } function convertObjectFieldNamesToJSConventions(object) { base.iterObjectFieldsRecursively( object, function(object, fieldName, fieldValue) { delete object[fieldName]; object[newFieldName] = fieldValue; return newFieldName; }); } function convertQuadSuffixedTypesToQuads(object) { base.iterObjectFieldsRecursively( object, function(object, fieldName, fieldValue) { }); } function convertObject(object) { convertObjectFieldNamesToJSConventions(object); convertQuadSuffixedTypesToQuads(object); } function moveRequiredFieldsFromArgsToToplevel(object, fields) { for (var i = 0; i < fields.length; i++) { var key = fields[i]; if (object.args[key] === undefined) throw Error('Expected field ' + key + ' not found in args'); if (object[key] !== undefined) throw Error('Field ' + key + ' already in object'); object[key] = object.args[key]; delete object.args[key]; } } function moveOptionalFieldsFromArgsToToplevel(object, fields) { for (var i = 0; i < fields.length; i++) { var key = fields[i]; if (object.args[key] === undefined) continue; if (object[key] !== undefined) throw Error('Field ' + key + ' already in object'); object[key] = object.args[key]; delete object.args[key]; } } function preInitializeObject(object) { preInitializeObjectInner(object.args, false); } function preInitializeObjectInner(object, hasRecursed) { if (!(object instanceof Object)) return; if (object instanceof Array) { for (var i = 0; i < object.length; i++) preInitializeObjectInner(object[i], true); return; } if (hasRecursed && (object instanceof tracing.trace_model.ObjectSnapshot || object instanceof tracing.trace_model.ObjectInstance)) return; for (var key in object) { var newKey = convertNameToJSConvention(key); if (newKey != key) { var value = object[key]; delete object[key]; object[newKey] = value; key = newKey; } // Convert objects with keys ending with Quad to base.Quad type. if (/Quad$/.test(key) && !(object[key] instanceof base.Quad)) { var q; try { q = base.Quad.from8Array(object[key]); } catch (e) { console.log(e); } object[key] = q; continue; } // Convert objects with keys ending with Rect to base.Rect type. if (/Rect$/.test(key) && !(object[key] instanceof base.Rect)) { var r; try { r = base.Rect.fromArray(object[key]); } catch (e) { console.log(e); } object[key] = r; } preInitializeObjectInner(object[key], true); } } return { preInitializeObject: preInitializeObject, convertNameToJSConvention: convertNameToJSConvention, moveRequiredFieldsFromArgsToToplevel: moveRequiredFieldsFromArgsToToplevel, moveOptionalFieldsFromArgsToToplevel: moveOptionalFieldsFromArgsToToplevel }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the LayerTreeHostImpl model-level objects. */ base.require('base.bbox2'); base.require('tracing.trace_model.object_instance'); base.require('cc.constants'); base.require('cc.layer_tree_impl'); base.require('cc.util'); base.exportTo('cc', function() { var constants = cc.constants; var ObjectSnapshot = tracing.trace_model.ObjectSnapshot; var ObjectInstance = tracing.trace_model.ObjectInstance; /** * @constructor */ function LayerTreeHostImplSnapshot() { ObjectSnapshot.apply(this, arguments); } LayerTreeHostImplSnapshot.prototype = { __proto__: ObjectSnapshot.prototype, preInitialize: function() { cc.preInitializeObject(this); }, initialize: function() { cc.moveRequiredFieldsFromArgsToToplevel( this, ['deviceViewportSize', 'activeTree']); cc.moveOptionalFieldsFromArgsToToplevel( this, ['pendingTree', 'tiles']); if (!this.tiles) this.tiles = []; this.activeTree.layerTreeHostImpl = this; this.activeTree.whichTree = constants.ACTIVE_TREE; if (this.pendingTree) { this.pendingTree.layerTreeHostImpl = this; this.pendingTree.whichTree = constants.PENDING_TREE; } }, /** * Get all of tile scales and their associated names. */ getContentsScaleNames: function() { var scales = {}; for (var i = 0; i < this.tiles.length; ++i) { var tile = this.tiles[i]; // Return scale -> scale name mappings. // Example: // 0.25 -> LOW_RESOLUTION // 1.0 -> HIGH_RESOLUTION // 0.75 -> NON_IDEAL_RESOLUTION scales[tile.contentsScale] = tile.resolution; } return scales; }, getTree: function(whichTree) { if (whichTree == constants.ACTIVE_TREE) return this.activeTree; if (whichTree == constants.PENDING_TREE) return this.pendingTree; throw new Exception('Unknown tree type + ' + whichTree); } }; ObjectSnapshot.register('cc::LayerTreeHostImpl', LayerTreeHostImplSnapshot); /** * @constructor */ function LayerTreeHostImplInstance() { ObjectInstance.apply(this, arguments); this.allLayersBBox_ = undefined; } LayerTreeHostImplInstance.prototype = { __proto__: ObjectInstance.prototype, get allContentsScales() { if (this.allContentsScales_) return this.allContentsScales_; var scales = {}; for (var tileID in this.allTileHistories_) { var tileHistory = this.allTileHistories_[tileID]; scales[tileHistory.contentsScale] = true; } this.allContentsScales_ = base.dictionaryKeys(scales); return this.allContentsScales_; }, get allLayersBBox() { if (this.allLayersBBox_) return this.allLayersBBox_; var bbox = new base.BBox2(); function handleTree(tree) { tree.renderSurfaceLayerList.forEach(function(layer) { bbox.addQuad(layer.layerQuad); }); } this.snapshots.forEach(function(lthi) { handleTree(lthi.activeTree); if (lthi.pendingTree) handleTree(lthi.pendingTree); }); this.allLayersBBox_ = bbox; return this.allLayersBBox_; } }; ObjectInstance.register('cc::LayerTreeHostImpl', LayerTreeHostImplInstance); return { LayerTreeHostImplSnapshot: LayerTreeHostImplSnapshot, LayerTreeHostImplInstance: LayerTreeHostImplInstance }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Helper functions for use in selection_analysis files. */ base.exportTo('tracing.analysis', function() { function tsRound(ts) { return Math.round(ts * 1000.0) / 1000.0; } return { tsRound: tsRound }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.exportTo('base', function() { function max(a, b) { if (a === undefined) return b; if (b === undefined) return a; return Math.max(a, b); } /** * This class implements an interval tree. * See: http://wikipedia.org/wiki/Interval_tree * * Internally the tree is a Red-Black tree. The insertion/colour is done using * the Left-leaning Red-Black Trees algorithm as described in: * http://www.cs.princeton.edu/~rs/talks/LLRB/LLRB.pdf * * @param {function} beginPositionCb Callback to retrieve the begin position. * @param {function} endPositionCb Callback to retrieve the end position. * * @constructor */ function IntervalTree(beginPositionCb, endPositionCb) { this.beginPositionCb_ = beginPositionCb; this.endPositionCb_ = endPositionCb; this.root_ = undefined; this.size_ = 0; } IntervalTree.prototype = { /** * Insert events into the interval tree. * * @param {Object} begin The left object. * @param {Object=} opt_end The end object, optional. If not provided the * begin object is assumed to also be the end object. */ insert: function(begin, opt_end) { var startPosition = this.beginPositionCb_(begin); var endPosition = this.endPositionCb_(opt_end || begin); var node = new IntervalTreeNode(begin, opt_end || begin, startPosition, endPosition); this.size_++; this.root_ = this.insertNode_(this.root_, node); this.root_.colour = Colour.BLACK; }, insertNode_: function(root, node) { if (root === undefined) return node; if (root.leftNode && root.leftNode.isRed && root.rightNode && root.rightNode.isRed) this.flipNodeColour_(root); if (node.key < root.key) root.leftNode = this.insertNode_(root.leftNode, node); else if (node.key === root.key) root.merge(node); else root.rightNode = this.insertNode_(root.rightNode, node); if (root.rightNode && root.rightNode.isRed && (root.leftNode === undefined || !root.leftNode.isRed)) root = this.rotateLeft_(root); if (root.leftNode && root.leftNode.isRed && root.leftNode.leftNode && root.leftNode.leftNode.isRed) root = this.rotateRight_(root); return root; }, rotateRight_: function(node) { var sibling = node.leftNode; node.leftNode = sibling.rightNode; sibling.rightNode = node; sibling.colour = node.colour; node.colour = Colour.RED; return sibling; }, rotateLeft_: function(node) { var sibling = node.rightNode; node.rightNode = sibling.leftNode; sibling.leftNode = node; sibling.colour = node.colour; node.colour = Colour.RED; return sibling; }, flipNodeColour_: function(node) { node.colour = this.flipColour_(node.colour); node.leftNode.colour = this.flipColour_(node.leftNode.colour); node.rightNode.colour = this.flipColour_(node.rightNode.colour); }, flipColour_: function(colour) { return colour === Colour.RED ? Colour.BLACK : Colour.RED; }, /* The high values are used to find intersection. It should be called after * all of the nodes are inserted. Doing it each insert is _slow_. */ updateHighValues: function() { this.updateHighValues_(this.root_); }, /* There is probably a smarter way to do this by starting from the inserted * node, but need to handle the rotations correctly. Went the easy route * for now. */ updateHighValues_: function(node) { if (node === undefined) return undefined; node.maxHighLeft = this.updateHighValues_(node.leftNode); node.maxHighRight = this.updateHighValues_(node.rightNode); return max(max(node.maxHighLeft, node.highValue), node.maxHighRight); }, /** * Retrieve all overlapping intervals. * * @param {number} lowValue The low value for the intersection interval. * @param {number} highValue The high value for the intersection interval. * @return {Array} All [begin, end] pairs inside intersecting intervals. */ findIntersection: function(lowValue, highValue) { if (lowValue === undefined || highValue === undefined) throw new Error('lowValue and highValue must be defined'); if ((typeof lowValue !== 'number') || (typeof highValue !== 'number')) throw new Error('lowValue and highValue must be numbers'); if (this.root_ === undefined) return []; return this.findIntersection_(this.root_, lowValue, highValue); }, findIntersection_: function(node, lowValue, highValue) { var ret = []; /* This node starts has a start point at or further right then highValue * so we know this node is out and all right children are out. Just need * to check left */ if (node.lowValue >= highValue) { if (!node.hasLeftNode) return []; return this.findIntersection_(node.leftNode, lowValue, highValue); } /* If we have a maximum left high value that is bigger then lowValue we * need to check left for matches */ if (node.maxHighLeft > lowValue) { ret = ret.concat( this.findIntersection_(node.leftNode, lowValue, highValue)); } /* We know that this node starts before highValue, if any of it's data * ends after lowValue we need to add those nodes */ if (node.highValue > lowValue) { for (var i = (node.data.length - 1); i >= 0; --i) { /* data nodes are sorted by high value, so as soon as we see one * before low value we're done. */ if (node.data[i].high < lowValue) break; ret.unshift([node.data[i].start, node.data[i].end]); } } /* check for matches in the right tree */ if (node.hasRightNode) { ret = ret.concat( this.findIntersection_(node.rightNode, lowValue, highValue)); } return ret; }, /** * Returns the number of nodes in the tree. */ get size() { return this.size_; }, /** * Returns the root node in the tree. */ get root() { return this.root_; }, /** * Dumps out the [lowValue, highValue] pairs for each node in depth-first * order. */ dump_: function() { if (this.root_ === undefined) return []; return this.dumpNode_(this.root_); }, dumpNode_: function(node) { var ret = {}; if (node.hasLeftNode) ret['left'] = this.dumpNode_(node.leftNode); ret['node'] = node.dump(); if (node.hasRightNode) ret['right'] = this.dumpNode_(node.rightNode); return ret; } }; var Colour = { RED: 'red', BLACK: 'black' }; function IntervalTreeNode(startObject, endObject, lowValue, highValue) { this.lowValue_ = lowValue; this.data_ = [{ start: startObject, end: endObject, high: highValue, low: lowValue }]; this.colour_ = Colour.RED; this.parentNode_ = undefined; this.leftNode_ = undefined; this.rightNode_ = undefined; this.maxHighLeft_ = undefined; this.maxHighRight_ = undefined; } IntervalTreeNode.prototype = { get colour() { return this.colour_; }, set colour(colour) { this.colour_ = colour; }, get key() { return this.lowValue_; }, get lowValue() { return this.lowValue_; }, get highValue() { return this.data_[this.data_.length - 1].high; }, set leftNode(left) { this.leftNode_ = left; }, get leftNode() { return this.leftNode_; }, get hasLeftNode() { return this.leftNode_ !== undefined; }, set rightNode(right) { this.rightNode_ = right; }, get rightNode() { return this.rightNode_; }, get hasRightNode() { return this.rightNode_ !== undefined; }, set parentNode(parent) { this.parentNode_ = parent; }, get parentNode() { return this.parentNode_; }, get isRootNode() { return this.parentNode_ === undefined; }, set maxHighLeft(high) { this.maxHighLeft_ = high; }, get maxHighLeft() { return this.maxHighLeft_; }, set maxHighRight(high) { this.maxHighRight_ = high; }, get maxHighRight() { return this.maxHighRight_; }, get data() { return this.data_; }, get isRed() { return this.colour_ === Colour.RED; }, merge: function(node) { this.data_ = this.data_.concat(node.data); this.data_.sort(function(a, b) { return a.high - b.high; }); }, dump: function() { if (this.data_.length === 1) return [this.data_[0].low, this.data[0].high]; return this.data_.map(function(d) { return [d.low, d.high]; }); } }; return { IntervalTree: IntervalTree }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.exportTo('tracing', function() { /** * @constructor The generic base class for filtering a TraceModel based on * various rules. The base class returns true for everything. */ function Filter() { } Filter.prototype = { __proto__: Object.prototype, matchCounter: function(counter) { return true; }, matchCpu: function(cpu) { return true; }, matchProcess: function(process) { return true; }, matchSlice: function(slice) { return true; }, matchThread: function(thread) { return true; } }; /** * @constructor A filter that matches objects by their name case insensitive. * .findAllObjectsMatchingFilter */ function TitleFilter(text) { Filter.call(this); this.text_ = text.toLowerCase(); if (!text.length) throw new Error('Filter text is empty.'); } TitleFilter.prototype = { __proto__: Filter.prototype, matchSlice: function(slice) { if (slice.title === undefined) return false; return slice.title.toLowerCase().indexOf(this.text_) !== -1; } }; /** * @constructor A filter that matches objects with the exact given title. */ function ExactTitleFilter(text) { Filter.call(this); this.text_ = text; if (!text.length) throw new Error('Filter text is empty.'); } ExactTitleFilter.prototype = { __proto__: Filter.prototype, matchSlice: function(slice) { return slice.title === this.text_; } }; return { TitleFilter: TitleFilter, ExactTitleFilter: ExactTitleFilter }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Base class for trace data importers. */ base.exportTo('tracing.importer', function() { function Importer() { } Importer.prototype = { __proto__: Object.prototype, /** * Called by the Model to extract one or more subtraces from the event data. */ extractSubtraces: function() { return []; }, /** * Called to import events into the Model. */ importEvents: function() { }, /** * Called by the Model after all other importers have imported their * events. */ finalizeImport: function() { }, /** * Called by the Model to join references between objects, after final * model bounds have been computed. */ joinRefs: function() { } }; return { Importer: Importer }; }); // Copyright (C) 2013: // Alex Russell // Yehuda Katz // // Use of this source code is governed by // http://www.apache.org/licenses/LICENSE-2.0 // FIXME(slightlyoff): // - Document "npm test" // - Change global name from "Promise" to something less conflicty (function(global, browserGlobal, underTest) { "use strict"; // FIXME(slighltyoff): // * aggregates + tests // * check on fast-forwarding underTest = !!underTest; // // Async Utilities // // Borrowed from RSVP.js var async; var MutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; var Promise; if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') { async = function(callback, binding) { process.nextTick(function() { callback.call(binding); }); }; } else if (MutationObserver) { var queue = []; var observer = new MutationObserver(function() { var toProcess = queue.slice(); queue = []; toProcess.forEach(function(tuple) { tuple[0].call(tuple[1]); }); }); var element = document.createElement('div'); observer.observe(element, { attributes: true }); // Chrome Memory Leak: https://bugs.webkit.org/show_bug.cgi?id=93661 window.addEventListener('unload', function(){ observer.disconnect(); observer = null; }); async = function(callback, binding) { queue.push([callback, binding]); element.setAttribute('drainQueue', 'drainQueue'); }; } else { async = function(callback, binding) { setTimeout(function() { callback.call(binding); }, 1); }; } // // Object Model Utilities // // defineProperties utilities var _readOnlyProperty = function(v) { return { enumerable: true, configurable: false, get: v }; }; var _method = function(v, e, c, w) { return { enumerable: !!(e || 0), configurable: !!(c || 1), writable: !!(w || 1), value: v || function() {} }; }; var _pseudoPrivate = function(v) { return _method(v, 0, 1, 0); }; var _public = function(v) { return _method(v, 1); }; // // Promises Utilities // var isThenable = function(any) { if (any === undefined) return false; try { var f = any.then; if (typeof f == "function") { return true; } } catch (e) { /*squelch*/ } return false; }; var AlreadyResolved = function(name) { Error.call(this, name); }; AlreadyResolved.prototype = Object.create(Error.prototype); var Backlog = function() { var bl = []; bl.pump = function(value) { async(function() { var l = bl.length; var x = 0; while(x < l) { x++; bl.shift()(value); } }); }; return bl; }; // // Resolver Constuctor // var Resolver = function(future, fulfillCallbacks, rejectCallbacks, setValue, setError, setState) { var isResolved = false; var resolver = this; var fulfill = function(value) { // console.log("queueing fulfill with:", value); async(function() { setState("fulfilled"); setValue(value); // console.log("fulfilling with:", value); fulfillCallbacks.pump(value); }); }; var reject = function(reason) { // console.log("queuing reject with:", reason); async(function() { setState("rejected"); setError(reason); // console.log("rejecting with:", reason); rejectCallbacks.pump(reason); }); }; var resolve = function(value) { if (isThenable(value)) { value.then(resolve, reject); return; } fulfill(value); }; var ifNotResolved = function(func, name) { return function(value) { if (!isResolved) { isResolved = true; func(value); } else { if (typeof console != "undefined") { console.error("Cannot resolve a Promise multiple times."); } } }; }; // Indirectly resolves the Promise, chaining any passed Promise's resolution this.resolve = ifNotResolved(resolve, "resolve"); // Directly fulfills the future, no matter what value's type is this.fulfill = ifNotResolved(fulfill, "fulfill"); // Rejects the future this.reject = ifNotResolved(reject, "reject"); this.cancel = function() { resolver.reject(new Error("Cancel")); }; this.timeout = function() { resolver.reject(new Error("Timeout")); }; if (underTest) { Object.defineProperties(this, { _isResolved: _readOnlyProperty(function() { return isResolved; }), }); } setState("pending"); }; // // Promise Constuctor // var Promise = function(init) { var fulfillCallbacks = new Backlog(); var rejectCallbacks = new Backlog(); var value; var error; var state = "pending"; if (underTest) { Object.defineProperties(this, { _value: _readOnlyProperty(function() { return value; }), _error: _readOnlyProperty(function() { return error; }), _state: _readOnlyProperty(function() { return state; }), }); } Object.defineProperties(this, { _addAcceptCallback: _pseudoPrivate( function(cb) { // console.log("adding fulfill callback:", cb); fulfillCallbacks.push(cb); if (state == "fulfilled") { fulfillCallbacks.pump(value); } } ), _addRejectCallback: _pseudoPrivate( function(cb) { // console.log("adding reject callback:", cb); rejectCallbacks.push(cb); if (state == "rejected") { rejectCallbacks.pump(error); } } ), }); var r = new Resolver(this, fulfillCallbacks, rejectCallbacks, function(v) { value = v; }, function(e) { error = e; }, function(s) { state = s; }) try { if (init) { init(r); } } catch(e) { r.reject(e); } }; // // Consructor // var isCallback = function(any) { return (typeof any == "function"); }; // Used in .then() var wrap = function(callback, resolver, disposition) { if (!isCallback(callback)) { // If we don't get a callback, we want to forward whatever resolution we get return resolver[disposition].bind(resolver); } return function() { try { var r = callback.apply(null, arguments); resolver.resolve(r); } catch(e) { // Exceptions reject the resolver resolver.reject(e); } }; }; var addCallbacks = function(onfulfill, onreject, scope) { if (isCallback(onfulfill)) { scope._addAcceptCallback(onfulfill); } if (isCallback(onreject)) { scope._addRejectCallback(onreject); } return scope; }; // // Prototype properties // Promise.prototype = Object.create(null, { "then": _public(function(onfulfill, onreject) { // The logic here is: // We return a new Promise whose resolution merges with the return from // onfulfill() or onerror(). If onfulfill() returns a Promise, we forward // the resolution of that future to the resolution of the returned // Promise. var f = this; return new Promise(function(r) { addCallbacks(wrap(onfulfill, r, "resolve"), wrap(onreject, r, "reject"), f); }); }), "catch": _public(function(onreject) { var f = this; return new Promise(function(r) { addCallbacks(null, wrap(onreject, r, "reject"), f); }); }), }); // // Statics // Promise.isThenable = isThenable; var toPromiseList = function(list) { return Array.prototype.slice.call(list).map(Promise.resolve); }; Promise.any = function(/*...futuresOrValues*/) { var futures = toPromiseList(arguments); return new Promise(function(r) { if (!futures.length) { r.reject("No futures passed to Promise.any()"); } else { var resolved = false; var firstSuccess = function(value) { if (resolved) { return; } resolved = true; r.resolve(value); }; var firstFailure = function(reason) { if (resolved) { return; } resolved = true; r.reject(reason); }; futures.forEach(function(f, idx) { f.then(firstSuccess, firstFailure); }); } }); }; Promise.every = function(/*...futuresOrValues*/) { var futures = toPromiseList(arguments); return new Promise(function(r) { if (!futures.length) { r.reject("No futures passed to Promise.every()"); } else { var values = new Array(futures.length); var count = 0; var accumulate = function(idx, v) { count++; values[idx] = v; if (count == futures.length) { r.resolve(values); } }; futures.forEach(function(f, idx) { f.then(accumulate.bind(null, idx), r.reject); }); } }); }; Promise.some = function() { var futures = toPromiseList(arguments); return new Promise(function(r) { if (!futures.length) { r.reject("No futures passed to Promise.some()"); } else { var count = 0; var accumulateFailures = function(e) { count++; if (count == futures.length) { r.reject(); } }; futures.forEach(function(f, idx) { f.then(r.resolve, accumulateFailures); }); } }); }; Promise.fulfill = function(value) { return new Promise(function(r) { r.fulfill(value); }); }; Promise.resolve = function(value) { return new Promise(function(r) { r.resolve(value); }); }; Promise.reject = function(reason) { return new Promise(function(r) { r.reject(reason); }); }; // // Export // global.Promise = Promise; })(this, (typeof window !== 'undefined') ? window : {}, this.runningUnderTest||false); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireRawScript('../third_party/Promises/polyfill/src/Promise.js'); base.exportTo('base', function() { var Promise = window.Promise; return { Promise: Promise }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.utils'); base.exportTo('base', function() { // Setting this to true will cause stack traces to get dumped into the // tasks. When an exception happens the original stack will be printed. // // NOTE: This should never be set committed as true. var recordRAFStacks = false; var pendingPreAFs = []; var pendingRAFs = []; var pendingIdleCallbacks = []; var currentRAFDispatchList = undefined; var rafScheduled = false; function scheduleRAF() { if (rafScheduled) return; rafScheduled = true; if (window.requestAnimationFrame) { window.requestAnimationFrame(processRequests); } else { var delta = Date.now() - window.performance.now(); window.webkitRequestAnimationFrame(function(domTimeStamp) { processRequests(domTimeStamp - delta); }); } } function onAnimationFrameError(e, opt_stack) { if (opt_stack) console.log(opt_stack); if (e.message) console.error(e.message, e.stack); else console.error(e); } function runTask(task, frameBeginTime) { try { task.callback.call(task.context, frameBeginTime); } catch (e) { base.onAnimationFrameError(e, task.stack); } } function processRequests(frameBeginTime) { // We assume that we want to do a maximum of 10ms optional work per frame. // Hopefully rAF will eventually pass this in for us. var rafCompletionDeadline = frameBeginTime + 10; rafScheduled = false; var currentPreAFs = pendingPreAFs; currentRAFDispatchList = pendingRAFs; pendingPreAFs = []; pendingRAFs = []; for (var i = 0; i < currentPreAFs.length; i++) runTask(currentPreAFs[i], frameBeginTime); while (currentRAFDispatchList.length > 0) runTask(currentRAFDispatchList.shift(), frameBeginTime); currentRAFDispatchList = undefined; while (pendingIdleCallbacks.length > 0 && window.performance.now() < rafCompletionDeadline) runTask(pendingIdleCallbacks.shift()); if (pendingIdleCallbacks.length > 0) scheduleRAF(); } function getStack_() { if (!recordRAFStacks) return ''; var stackLines = base.stackTrace(); // Strip off getStack_. stackLines.shift(); return stackLines.join('\n'); } function requestPreAnimationFrame(callback, opt_this) { pendingPreAFs.push({ callback: callback, context: opt_this || window, stack: getStack_()}); scheduleRAF(); } function requestAnimationFrameInThisFrameIfPossible(callback, opt_this) { if (!currentRAFDispatchList) { requestAnimationFrame(callback, opt_this); return; } currentRAFDispatchList.push({ callback: callback, context: opt_this || window, stack: getStack_()}); return; } function requestAnimationFrame(callback, opt_this) { pendingRAFs.push({ callback: callback, context: opt_this || window, stack: getStack_()}); scheduleRAF(); } function requestIdleCallback(callback, opt_this) { pendingIdleCallbacks.push({ callback: callback, context: opt_this || window, stack: getStack_()}); scheduleRAF(); } function forcePendingRAFTasksToRun(frameBeginTime) { if (!rafScheduled) return; processRequests(frameBeginTime); } return { onAnimationFrameError: onAnimationFrameError, requestPreAnimationFrame: requestPreAnimationFrame, requestAnimationFrame: requestAnimationFrame, requestAnimationFrameInThisFrameIfPossible: requestAnimationFrameInThisFrameIfPossible, requestIdleCallback: requestIdleCallback, forcePendingRAFTasksToRun: forcePendingRAFTasksToRun }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.raf'); base.require('base.promise'); base.exportTo('tracing.importer', function() { /** * A task is a combination of a run callback, a set of subtasks, and an after * task. * * When executed, a task does the following things: * 1. Runs its callback * 2. Runs its subtasks * 3. Runs its after callback. * * The list of subtasks and after task can be mutated inside step #1 but as * soon as the task's callback returns, the subtask list and after task is * fixed and cannot be changed again. * * Use task.after().after().after() to describe the toplevel passes that make * up your computation. Then, use subTasks to add detail to each subtask as it * runs. For example: * var pieces = []; * taskA = new Task(function() { pieces = getPieces(); }); * taskA.after(function(taskA) { * pieces.forEach(function(piece) { * taskA.subTask(function(taskB) { piece.process(); }, this); * }); * }); * * @constructor */ function Task(runCb, thisArg) { if (thisArg === undefined) throw new Error('Almost certainly, you meant to pass a thisArg.'); this.runCb_ = runCb; this.thisArg_ = thisArg; this.afterTask_ = undefined; this.subTasks_ = []; } Task.prototype = { /* * See constructor documentation on semantics of subtasks. */ subTask: function(cb, thisArg) { if (cb instanceof Task) this.subTasks_.push(cb); else this.subTasks_.push(new Task(cb, thisArg)); return this.subTasks_[this.subTasks_.length - 1]; }, /** * Runs the current task and returns the task that should be executed next. */ run: function() { this.runCb_.call(this.thisArg_, this); var subTasks = this.subTasks_; this.subTasks_ = undefined; // Prevent more subTasks from being posted. if (!subTasks.length) return this.afterTask_; // If there are subtasks, then we want to execute all the subtasks and // then this task's afterTask. To make this happen, we update the // afterTask of all the subtasks so the point upward to each other, e.g. // subTask[0].afterTask to subTask[1] and so on. Then, the last subTask's // afterTask points at this task's afterTask. for (var i = 1; i < subTasks.length; i++) subTasks[i - 1].afterTask_ = subTasks[i]; subTasks[subTasks.length - 1].afterTask_ = this.afterTask_; return subTasks[0]; }, /* * See constructor documentation on semantics of after tasks. */ after: function(cb, thisArg) { if (this.afterTask_) throw new Error('Has an after task already'); if (cb instanceof Task) this.afterTask_ = cb; else this.afterTask_ = new Task(cb, thisArg); return this.afterTask_; } }; Task.RunSynchronously = function(task) { var curTask = task; while (curTask) curTask = curTask.run(); } /** * Runs a task using raf.requestIdleCallback, returning * a promise for its completion. */ Task.RunWhenIdle = function(task) { return new base.Promise(function(resolver) { var curTask = task; function runAnother() { curTask = curTask.run(); if (curTask) { base.requestIdleCallback(runAnother); return; } resolver.resolve(); } base.requestIdleCallback(runAnother); }); } return { Task: Task }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.iteration_helpers'); base.require('base.sorted_array_utils'); base.require('tracing.trace_model.event'); base.exportTo('tracing.trace_model', function() { function CounterSample(series, timestamp, value) { tracing.trace_model.Event.call(this); this.series_ = series; this.timestamp_ = timestamp; this.value_ = value; } CounterSample.groupByTimestamp = function(samples) { var samplesByTimestamp = {}; for (var i = 0; i < samples.length; i++) { var sample = samples[i]; var ts = sample.timestamp; if (!samplesByTimestamp[ts]) samplesByTimestamp[ts] = []; samplesByTimestamp[ts].push(sample); } var timestamps = base.dictionaryKeys(samplesByTimestamp); timestamps.sort(); var groups = []; for (var i = 0; i < timestamps.length; i++) { var ts = timestamps[i]; var group = samplesByTimestamp[ts]; group.sort(function(x, y) { return x.series.seriesIndex - y.series.seriesIndex; }); groups.push(group); } return groups; } CounterSample.prototype = { __proto__: tracing.trace_model.Event.prototype, get series() { return this.series_; }, get timestamp() { return this.timestamp_; }, get value() { return this.value_; }, set timestamp(timestamp) { this.timestamp_ = timestamp; }, addBoundsToRange: function(range) { range.addValue(this.timestamp); }, toJSON: function() { var obj = new Object(); var keys = Object.keys(this); for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (typeof this[key] == 'function') continue; if (key == 'series_') { obj[key] = this[key].guid; continue; } obj[key] = this[key]; } return obj; }, getSampleIndex: function() { return base.findLowIndexInSortedArray( this.series.timestamps, function(x) { return x; }, this.timestamp_); } }; return { CounterSample: CounterSample }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.trace_model.counter_sample'); /** * @fileoverview Provides the CounterSeries class. */ base.exportTo('tracing.trace_model', function() { var CounterSample = tracing.trace_model.CounterSample; function CounterSeries(name, color) { this.guid_ = base.GUID.allocate(); this.name_ = name; this.color_ = color; this.timestamps_ = []; this.samples_ = []; // Set by counter.addSeries this.counter = undefined; this.seriesIndex = undefined; } CounterSeries.prototype = { __proto__: Object.prototype, toJSON: function() { var obj = new Object(); var keys = Object.keys(this); for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (typeof this[key] == 'function') continue; if (key == 'counter') { obj[key] = this[key].guid; continue; } obj[key] = this[key]; } return obj; }, get length() { return this.timestamps_.length; }, get name() { return this.name_; }, get color() { return this.color_; }, get samples() { return this.samples_; }, get timestamps() { return this.timestamps_; }, getSample: function(idx) { return this.samples_[idx]; }, getTimestamp: function(idx) { return this.timestamps_[idx]; }, addSample: function(ts, val) { this.timestamps_.push(ts); var sample = new CounterSample(this, ts, val); this.samples_.push(sample); return sample; }, getStatistics: function(sampleIndices) { var sum = 0; var min = Number.MAX_VALUE; var max = -Number.MAX_VALUE; for (var i = 0; i < sampleIndices.length; ++i) { var sample = this.getSample(sampleIndices[i]).value; sum += sample; min = Math.min(sample, min); max = Math.max(sample, max); } return { min: min, max: max, avg: (sum / sampleIndices.length), start: this.getSample(sampleIndices[0]).value, end: this.getSample(sampleIndices.length - 1).value }; }, shiftTimestampsForward: function(amount) { for (var i = 0; i < this.timestamps_.length; ++i) { this.timestamps_[i] += amount; this.samples_[i].timestamp = this.timestamps_[i]; } }, iterateAllEvents: function(callback) { this.samples_.forEach(callback); } }; return { CounterSeries: CounterSeries }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.guid'); base.require('base.range'); base.require('tracing.trace_model.counter_series'); /** * @fileoverview Provides the Counter class. */ base.exportTo('tracing.trace_model', function() { /** * Stores all the samples for a given counter. * @constructor */ function Counter(parent, id, category, name) { this.guid_ = base.GUID.allocate(); this.parent = parent; this.id = id; this.category = category || ''; this.name = name; this.series_ = []; this.totals = []; this.bounds = new base.Range(); } Counter.prototype = { __proto__: Object.prototype, /* * @return {Number} A globally unique identifier for this counter. */ get guid() { return this.guid_; }, toJSON: function() { var obj = new Object(); var keys = Object.keys(this); for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (typeof this[key] == 'function') continue; if (key == 'parent') { obj[key] = this[key].guid; continue; } obj[key] = this[key]; } return obj; }, set timestamps(arg) { throw new Error('Bad counter API. No cookie.'); }, set seriesNames(arg) { throw new Error('Bad counter API. No cookie.'); }, set seriesColors(arg) { throw new Error('Bad counter API. No cookie.'); }, set samples(arg) { throw new Error('Bad counter API. No cookie.'); }, addSeries: function(series) { series.counter = this; series.seriesIndex = this.series_.length; this.series_.push(series); return series; }, getSeries: function(idx) { return this.series_[idx]; }, get series() { return this.series_; }, get numSeries() { return this.series_.length; }, get numSamples() { if (this.series_.length === 0) return 0; return this.series_[0].length; }, get timestamps() { if (this.series_.length === 0) return []; return this.series_[0].timestamps; }, /** * Obtains min, max, avg, values, start, and end for different series for * a given counter * getSampleStatistics([0,1]) * The statistics objects that this returns are an array of objects, one * object for each series for the counter in the form: * {min: minVal, max: maxVal, avg: avgVal, start: startVal, end: endVal} * * @param {Array.} Indices to summarize. * @return {Object} An array of statistics. Each element in the array * has data for one of the series in the selected counter. */ getSampleStatistics: function(sampleIndices) { sampleIndices.sort(); var ret = []; this.series_.forEach(function(series) { ret.push(series.getStatistics(sampleIndices)); }); return ret; }, /** * Shifts all the timestamps inside this counter forward by the amount * specified. */ shiftTimestampsForward: function(amount) { for (var i = 0; i < this.series_.length; ++i) this.series_[i].shiftTimestampsForward(amount); }, /** * Updates the bounds for this counter based on the samples it contains. */ updateBounds: function() { this.totals = []; this.maxTotal = 0; this.bounds.reset(); if (this.series_.length === 0) return; var firstSeries = this.series_[0]; var lastSeries = this.series_[this.series_.length - 1]; this.bounds.addValue(firstSeries.getTimestamp(0)); this.bounds.addValue(lastSeries.getTimestamp(lastSeries.length - 1)); var numSeries = this.numSeries; this.maxTotal = -Infinity; // Sum the samples at each timestamp. // Note, this assumes that all series have all timestamps. for (var i = 0; i < firstSeries.length; ++i) { var total = 0; this.series_.forEach(function(series) { total += series.getSample(i).value; this.totals.push(total); }.bind(this)); this.maxTotal = Math.max(total, this.maxTotal); } }, iterateAllEvents: function(callback) { for (var i = 0; i < this.series_.length; i++) this.series_[i].iterateAllEvents(callback); } }; /** * Comparison between counters that orders by parent.compareTo, then name. */ Counter.compare = function(x, y) { var tmp = x.parent.compareTo(y); if (tmp != 0) return tmp; var tmp = x.name.localeCompare(y.name); if (tmp == 0) return x.tid - y.tid; return tmp; }; return { Counter: Counter }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.guid'); base.require('tracing.trace_model.event'); /** * @fileoverview Provides the TimedEvent class. */ base.exportTo('tracing.trace_model', function() { /** * A TimedEvent is the base type for any piece of data in the trace model with * a specific start and duration. * * @constructor */ function TimedEvent(start) { tracing.trace_model.Event.call(this); this.start = start; this.duration = 0; } TimedEvent.prototype = { __proto__: tracing.trace_model.Event.prototype, get end() { return this.start + this.duration; }, addBoundsToRange: function(range) { range.addValue(this.start); range.addValue(this.end); } }; return { TimedEvent: TimedEvent }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.trace_model.timed_event'); /** * @fileoverview Provides the Slice class. */ base.exportTo('tracing.trace_model', function() { /** * A Slice represents an interval of time plus parameters associated * with that interval. * * @constructor */ function Slice(category, title, colorId, start, args, opt_duration) { tracing.trace_model.TimedEvent.call(this, start); this.category = category || ''; this.title = title; this.colorId = colorId; this.args = args; this.didNotFinish = false; if (opt_duration !== undefined) this.duration = opt_duration; } Slice.prototype = { __proto__: tracing.trace_model.TimedEvent.prototype, get analysisTypeName() { return this.title; } }; return { Slice: Slice }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the Cpu class. */ base.require('base.range'); base.require('tracing.trace_model.slice'); base.require('tracing.trace_model.counter'); base.exportTo('tracing.trace_model', function() { var Counter = tracing.trace_model.Counter; var Slice = tracing.trace_model.Slice; /** * A CpuSlice represents an slice of time on a CPU. * * @constructor */ function CpuSlice(cat, title, colorId, start, args, opt_duration) { Slice.apply(this, arguments); this.threadThatWasRunning = undefined; this.cpu = undefined; } CpuSlice.prototype = { __proto__: Slice.prototype, get analysisTypeName() { return 'tracing.analysis.CpuSlice'; }, toJSON: function() { var obj = new Object(); var keys = Object.keys(this); for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (typeof this[key] == 'function') continue; if (key == 'cpu' || key == 'threadThatWasRunning') { if (this[key]) obj[key] = this[key].guid; continue; } obj[key] = this[key]; } return obj; }, getAssociatedTimeslice: function() { if (!this.threadThatWasRunning) return undefined; var timeSlices = this.threadThatWasRunning.timeSlices; for (var i = 0; i < timeSlices.length; i++) { var timeSlice = timeSlices[i]; if (timeSlice.start !== this.start) continue; if (timeSlice.duration !== this.duration) continue; return timeSlice; } return undefined; } }; /** * A ThreadTimeSlice is a slice of time on a specific thread where that thread * was running on a specific CPU, or in a specific sleep state. * * As a thread switches moves through its life, it sometimes goes to sleep and * can't run. Other times, its runnable but isn't actually assigned to a CPU. * Finally, sometimes it gets put on a CPU to actually execute. Each of these * states is represented by a ThreadTimeSlice: * * Sleeping or runnable: cpuOnWhichThreadWasRunning is undefined * Running: cpuOnWhichThreadWasRunning is set. * * @constructor */ function ThreadTimeSlice( thread, cat, title, colorId, start, args, opt_duration) { Slice.call(this, cat, title, colorId, start, args, opt_duration); this.thread = thread; this.cpuOnWhichThreadWasRunning = undefined; } ThreadTimeSlice.prototype = { __proto__: Slice.prototype, get analysisTypeName() { return 'tracing.analysis.ThreadTimeSlice'; }, toJSON: function() { var obj = new Object(); var keys = Object.keys(this); for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (typeof this[key] == 'function') continue; if (key == 'thread' || key == 'cpuOnWhichThreadWasRunning') { if (this[key]) obj[key] = this[key].guid; continue; } obj[key] = this[key]; } return obj; }, getAssociatedCpuSlice: function() { if (!this.cpuOnWhichThreadWasRunning) return undefined; var cpuSlices = this.cpuOnWhichThreadWasRunning.slices; for (var i = 0; i < cpuSlices.length; i++) { var cpuSlice = cpuSlices[i]; if (cpuSlice.start !== this.start) continue; if (cpuSlice.duration !== this.duration) continue; return cpuSlice; } return undefined; }, getCpuSliceThatTookCpu: function() { if (this.cpuOnWhichThreadWasRunning) return undefined; var curIndex = this.thread.indexOfTimeSlice(this); var cpuSliceWhenLastRunning; while (curIndex >= 0) { var curSlice = this.thread.timeSlices[curIndex]; if (!curSlice.cpuOnWhichThreadWasRunning) { curIndex--; continue; } cpuSliceWhenLastRunning = curSlice.getAssociatedCpuSlice(); break; } if (!cpuSliceWhenLastRunning) return undefined; var cpu = cpuSliceWhenLastRunning.cpu; var indexOfSliceOnCpuWhenLastRunning = cpu.indexOf(cpuSliceWhenLastRunning); var nextRunningSlice = cpu.slices[indexOfSliceOnCpuWhenLastRunning + 1]; if (!nextRunningSlice) return undefined; if (Math.abs(nextRunningSlice.start - cpuSliceWhenLastRunning.end) < 0.00001) return nextRunningSlice; return undefined; } }; /** * The Cpu represents a Cpu from the kernel's point of view. * @constructor */ function Cpu(number) { this.cpuNumber = number; this.slices = []; this.counters = {}; this.bounds = new base.Range(); }; Cpu.prototype = { /** * @return {TimlineCounter} The counter on this process named 'name', * creating it if it doesn't exist. */ getOrCreateCounter: function(cat, name) { var id; if (cat.length) id = cat + '.' + name; else id = name; if (!this.counters[id]) this.counters[id] = new Counter(this, id, cat, name); return this.counters[id]; }, /** * Shifts all the timestamps inside this CPU forward by the amount * specified. */ shiftTimestampsForward: function(amount) { for (var sI = 0; sI < this.slices.length; sI++) this.slices[sI].start = (this.slices[sI].start + amount); for (var id in this.counters) this.counters[id].shiftTimestampsForward(amount); }, /** * Updates the range based on the current slices attached to the cpu. */ updateBounds: function() { this.bounds.reset(); if (this.slices.length) { this.bounds.addValue(this.slices[0].start); this.bounds.addValue(this.slices[this.slices.length - 1].end); } for (var id in this.counters) { this.counters[id].updateBounds(); this.bounds.addRange(this.counters[id].bounds); } }, addCategoriesToDict: function(categoriesDict) { for (var i = 0; i < this.slices.length; i++) categoriesDict[this.slices[i].category] = true; for (var id in this.counters) categoriesDict[this.counters[id].category] = true; }, get userFriendlyName() { return 'CPU ' + this.cpuNumber; }, /* * Returns the index of the slice in the CPU's slices, or undefined. */ indexOf: function(cpuSlice) { var i = base.findLowIndexInSortedArray( this.slices, function(slice) { return slice.start; }, cpuSlice.start); if (this.slices[i] !== cpuSlice) return undefined; return i; }, iterateAllEvents: function(callback) { this.slices.forEach(callback); for (var id in this.counters) this.counters[id].iterateAllEvents(callback); } }; /** * Comparison between processes that orders by cpuNumber. */ Cpu.compare = function(x, y) { return x.cpuNumber - y.cpuNumber; }; return { Cpu: Cpu, CpuSlice: CpuSlice, ThreadTimeSlice: ThreadTimeSlice }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the TimeToObjectInstanceMap class. */ base.require('base.range'); base.require('base.sorted_array_utils'); base.exportTo('tracing.trace_model', function() { /** * Tracks all the instances associated with a given ID over its lifetime. * * An id can be used multiple times throughout a trace, referring to different * objects at different times. This data structure does the bookkeeping to * figure out what ObjectInstance is referred to at a given timestamp. * * @constructor */ function TimeToObjectInstanceMap(createObjectInstanceFunction, parent, id) { this.createObjectInstanceFunction_ = createObjectInstanceFunction; this.parent = parent; this.id = id; this.instances = []; } TimeToObjectInstanceMap.prototype = { idWasCreated: function(category, name, ts) { if (this.instances.length == 0) { this.instances.push(this.createObjectInstanceFunction_( this.parent, this.id, category, name, ts)); this.instances[0].creationTsWasExplicit = true; return this.instances[0]; } var lastInstance = this.instances[this.instances.length - 1]; if (ts < lastInstance.deletionTs) { throw new Error('Mutation of the TimeToObjectInstanceMap must be ' + 'done in ascending timestamp order.'); } lastInstance = this.createObjectInstanceFunction_( this.parent, this.id, category, name, ts); lastInstance.creationTsWasExplicit = true; this.instances.push(lastInstance); return lastInstance; }, addSnapshot: function(category, name, ts, args) { if (this.instances.length == 0) { this.instances.push(this.createObjectInstanceFunction_( this.parent, this.id, category, name, ts)); } var i = base.findLowIndexInSortedIntervals( this.instances, function(inst) { return inst.creationTs; }, function(inst) { return inst.deletionTs - inst.creationTs; }, ts); var instance; if (i < 0) { instance = this.instances[0]; if (ts > instance.deletionTs || instance.creationTsWasExplicit) { throw new Error( 'At the provided timestamp, no instance was still alive'); } if (instance.snapshots.length != 0) { throw new Error( 'Cannot shift creationTs forward, ' + 'snapshots have been added. First snap was at ts=' + instance.snapshots[0].ts + ' and creationTs was ' + instance.creationTs); } instance.creationTs = ts; } else if (i >= this.instances.length) { instance = this.instances[this.instances.length - 1]; if (ts >= instance.deletionTs) { // The snap is added after our oldest and deleted instance. This means // that this is a new implicit instance. instance = this.createObjectInstanceFunction_( this.parent, this.id, category, name, ts); this.instances.push(instance); } else { // If the ts is before the last objects deletion time, then the caller // is trying to add a snapshot when there may have been an instance // alive. In that case, try to move an instance's creationTs to // include this ts, provided that it has an implicit creationTs. // Search backward from the right for an instance that was definitely // deleted before this ts. Any time an instance is found that has a // moveable creationTs var lastValidIndex; for (var i = this.instances.length - 1; i >= 0; i--) { var tmp = this.instances[i]; if (ts >= tmp.deletionTs) break; if (tmp.creationTsWasExplicit == false && tmp.snapshots.length == 0) lastValidIndex = i; } if (lastValidIndex === undefined) { throw new Error( 'Cannot add snapshot. No instance was alive that was mutable.'); } instance = this.instances[lastValidIndex]; instance.creationTs = ts; } } else { instance = this.instances[i]; } return instance.addSnapshot(ts, args); }, get lastInstance() { if (this.instances.length == 0) return undefined; return this.instances[this.instances.length - 1]; }, idWasDeleted: function(category, name, ts) { if (this.instances.length == 0) { this.instances.push(this.createObjectInstanceFunction_( this.parent, this.id, category, name, ts)); } var lastInstance = this.instances[this.instances.length - 1]; if (ts < lastInstance.creationTs) throw new Error('Cannot delete a id before it was crated'); if (lastInstance.deletionTs == Number.MAX_VALUE) { lastInstance.wasDeleted(ts); return lastInstance; } if (ts < lastInstance.deletionTs) throw new Error('id was already deleted earlier.'); // A new instance was deleted with no snapshots in-between. // Create an instance then kill it. lastInstance = this.createObjectInstanceFunction_( this.parent, this.id, category, name, ts); this.instances.push(lastInstance); return lastInstance; }, getInstanceAt: function(ts) { var i = base.findLowIndexInSortedIntervals( this.instances, function(inst) { return inst.creationTs; }, function(inst) { return inst.deletionTs - inst.creationTs; }, ts); if (i < 0) { if (this.instances[0].creationTsWasExplicit) return undefined; return this.instances[0]; } else if (i >= this.instances.length) { return undefined; } return this.instances[i]; } }; return { TimeToObjectInstanceMap: TimeToObjectInstanceMap }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the ObjectCollection class. */ base.require('base.utils'); base.require('base.range'); base.require('base.sorted_array_utils'); base.require('tracing.trace_model.object_instance'); base.require('tracing.trace_model.time_to_object_instance_map'); base.exportTo('tracing.trace_model', function() { var ObjectInstance = tracing.trace_model.ObjectInstance; /** * A collection of object instances and their snapshots, accessible by id and * time, or by object name. * * @constructor */ function ObjectCollection(parent) { this.parent = parent; this.bounds = new base.Range(); this.instanceMapsById_ = {}; // id -> TimeToObjectInstanceMap this.instancesByTypeName_ = {}; this.createObjectInstance_ = this.createObjectInstance_.bind(this); } ObjectCollection.prototype = { __proto__: Object.prototype, createObjectInstance_: function(parent, id, category, name, creationTs) { var constructor = tracing.trace_model.ObjectInstance.getConstructor(name); var instance = new constructor(parent, id, category, name, creationTs); var typeName = instance.typeName; var instancesOfTypeName = this.instancesByTypeName_[typeName]; if (!instancesOfTypeName) { instancesOfTypeName = []; this.instancesByTypeName_[typeName] = instancesOfTypeName; } instancesOfTypeName.push(instance); return instance; }, getOrCreateInstanceMap_: function(id) { var instanceMap = this.instanceMapsById_[id]; if (instanceMap) return instanceMap; instanceMap = new tracing.trace_model.TimeToObjectInstanceMap( this.createObjectInstance_, this.parent, id); this.instanceMapsById_[id] = instanceMap; return instanceMap; }, idWasCreated: function(id, category, name, ts) { var instanceMap = this.getOrCreateInstanceMap_(id); return instanceMap.idWasCreated(category, name, ts); }, addSnapshot: function(id, category, name, ts, args) { var instanceMap = this.getOrCreateInstanceMap_(id, category, name, ts); var snapshot = instanceMap.addSnapshot(category, name, ts, args); if (snapshot.objectInstance.category != category) { throw new Error('Added snapshot with different category ' + 'than when it was created'); } if (snapshot.objectInstance.name != name) { throw new Error('Added snapshot with different name than ' + 'when it was created'); } return snapshot; }, idWasDeleted: function(id, category, name, ts) { var instanceMap = this.getOrCreateInstanceMap_(id, category, name, ts); var deletedInstance = instanceMap.idWasDeleted(category, name, ts); if (!deletedInstance) return; if (deletedInstance.category != category) { throw new Error('Deleting an object with a different category ' + 'than when it was created'); } if (deletedInstance.name != name) { throw new Error('Deleting an object with a different name than ' + 'when it was created'); } }, autoDeleteObjects: function(maxTimestamp) { base.iterItems(this.instanceMapsById_, function(id, i2imap) { var lastInstance = i2imap.lastInstance; if (lastInstance.deletionTs != Number.MAX_VALUE) return; i2imap.idWasDeleted( lastInstance.category, lastInstance.name, maxTimestamp); // idWasDeleted will cause lastInstance.deletionTsWasExplicit to be set // to true. Unset it here. lastInstance.deletionTsWasExplicit = false; }); }, getObjectInstanceAt: function(id, ts) { var instanceMap = this.instanceMapsById_[id]; if (!instanceMap) return undefined; return instanceMap.getInstanceAt(ts); }, getSnapshotAt: function(id, ts) { var instance = this.getObjectInstanceAt(id, ts); if (!instance) return undefined; return instance.getSnapshotAt(ts); }, iterObjectInstances: function(iter, opt_this) { opt_this = opt_this || this; base.iterItems(this.instanceMapsById_, function(id, i2imap) { i2imap.instances.forEach(iter, opt_this); }); }, getAllObjectInstances: function() { var instances = []; this.iterObjectInstances(function(i) { instances.push(i); }); return instances; }, getAllInstancesNamed: function(name) { return this.instancesByTypeName_[name]; }, getAllInstancesByTypeName: function() { return this.instancesByTypeName_; }, preInitializeAllObjects: function() { this.iterObjectInstances(function(instance) { instance.preInitialize(); }); }, initializeAllObjects: function() { this.iterObjectInstances(function(instance) { instance.initialize(); }); }, initializeInstances: function() { this.iterObjectInstances(function(instance) { instance.initialize(); }); }, updateBounds: function() { this.bounds.reset(); this.iterObjectInstances(function(instance) { instance.updateBounds(); this.bounds.addRange(instance.bounds); }, this); }, shiftTimestampsForward: function(amount) { this.iterObjectInstances(function(instance) { instance.shiftTimestampsForward(amount); }); }, addCategoriesToDict: function(categoriesDict) { this.iterObjectInstances(function(instance) { categoriesDict[instance.category] = true; }); }, toJSON: function() { // TODO(nduca): Implement this if we need it. return {}; }, iterateAllEvents: function(callback) { this.iterObjectInstances(function(instance) { callback(instance); instance.snapshots.forEach(callback); }); } }; return { ObjectCollection: ObjectCollection }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.trace_model.slice'); /** * @fileoverview Provides the AsyncSlice class. */ base.exportTo('tracing.trace_model', function() { /** * A AsyncSlice represents an interval of time during which an * asynchronous operation is in progress. An AsyncSlice consumes no CPU time * itself and so is only associated with Threads at its start and end point. * * @constructor */ function AsyncSlice(category, title, colorId, start, args) { tracing.trace_model.Slice.apply(this, arguments); }; AsyncSlice.prototype = { __proto__: tracing.trace_model.Slice.prototype, toJSON: function() { var obj = new Object(); var keys = Object.keys(this); for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (typeof this[key] == 'function') continue; if (key == 'startThread' || key == 'endThread') { obj[key] = this[key].guid; continue; } obj[key] = this[key]; } return obj; }, id: undefined, startThread: undefined, endThread: undefined, subSlices: undefined }; return { AsyncSlice: AsyncSlice }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the AsyncSliceGroup class. */ base.require('base.range'); base.require('tracing.trace_model.async_slice'); base.exportTo('tracing.trace_model', function() { /** * A group of AsyncSlices. * @constructor */ function AsyncSliceGroup() { this.slices = []; this.bounds = new base.Range(); } AsyncSliceGroup.prototype = { __proto__: Object.prototype, /** * Helper function that pushes the provided slice onto the slices array. */ push: function(slice) { this.slices.push(slice); }, /** * @return {Number} The number of slices in this group. */ get length() { return this.slices.length; }, /** * Shifts all the timestamps inside this group forward by the amount * specified. */ shiftTimestampsForward: function(amount) { for (var sI = 0; sI < this.slices.length; sI++) { var slice = this.slices[sI]; slice.start = (slice.start + amount); for (var sJ = 0; sJ < slice.subSlices.length; sJ++) slice.subSlices[sJ].start += amount; } }, /** * Updates the bounds for this group based on the slices it contains. */ updateBounds: function() { this.bounds.reset(); for (var i = 0; i < this.slices.length; i++) { this.bounds.addValue(this.slices[i].start); this.bounds.addValue(this.slices[i].end); } }, /** * Breaks up this group into slices based on start thread. * * @return {Array} An array of AsyncSliceGroups where each group has * slices that started on the same thread. */ computeSubGroups: function() { var subGroupsByGUID = {}; for (var i = 0; i < this.slices.length; ++i) { var slice = this.slices[i]; var sliceGUID = slice.startThread.guid; if (!subGroupsByGUID[sliceGUID]) subGroupsByGUID[sliceGUID] = new AsyncSliceGroup(); subGroupsByGUID[sliceGUID].slices.push(slice); } var groups = []; for (var guid in subGroupsByGUID) { var group = subGroupsByGUID[guid]; group.updateBounds(); groups.push(group); } return groups; }, iterateAllEvents: function(callback) { for (var i = 0; i < this.slices.length; i++) { var slice = this.slices[i]; callback(slice); if (slice.subSlices) slice.subSlices.forEach(callback); } } }; return { AsyncSliceGroup: AsyncSliceGroup }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.trace_model.timed_event'); /** * @fileoverview Provides the Sample class. */ base.exportTo('tracing.trace_model', function() { /** * A Sample represents a sample taken at an instant in time * plus parameters associated with that sample. * * @constructor */ function Sample(category, title, colorId, start, args) { tracing.trace_model.TimedEvent.call(this, start); this.category = category || ''; this.title = title; this.colorId = colorId; this.args = args; } Sample.prototype = { __proto__: tracing.trace_model.TimedEvent.prototype }; return { Sample: Sample }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.trace_model.event'); /** * @fileoverview Provides color scheme related functions. */ base.exportTo('tracing', function() { var SelectionState = tracing.trace_model.SelectionState; // The color palette is split in half, with the upper // half of the palette being the "highlighted" verison // of the base color. So, color 7's highlighted form is // 7 + (palette.length / 2). // // These bright versions of colors are automatically generated // from the base colors. // // Within the color palette, there are "regular" colors, // which can be used for random color selection, and // reserved colors, which are used when specific colors // need to be used, e.g. where red is desired. var paletteBase = [ {r: 138, g: 113, b: 152}, {r: 175, g: 112, b: 133}, {r: 127, g: 135, b: 225}, {r: 93, g: 81, b: 137}, {r: 116, g: 143, b: 119}, {r: 178, g: 214, b: 122}, {r: 87, g: 109, b: 147}, {r: 119, g: 155, b: 95}, {r: 114, g: 180, b: 160}, {r: 132, g: 85, b: 103}, {r: 157, g: 210, b: 150}, {r: 148, g: 94, b: 86}, {r: 164, g: 108, b: 138}, {r: 139, g: 191, b: 150}, {r: 110, g: 99, b: 145}, {r: 80, g: 129, b: 109}, {r: 125, g: 140, b: 149}, {r: 93, g: 124, b: 132}, {r: 140, g: 85, b: 140}, {r: 104, g: 163, b: 162}, {r: 132, g: 141, b: 178}, {r: 131, g: 105, b: 147}, {r: 135, g: 183, b: 98}, {r: 152, g: 134, b: 177}, {r: 141, g: 188, b: 141}, {r: 133, g: 160, b: 210}, {r: 126, g: 186, b: 148}, {r: 112, g: 198, b: 205}, {r: 180, g: 122, b: 195}, {r: 203, g: 144, b: 152}, // Reserved Entires {r: 182, g: 125, b: 143}, {r: 126, g: 200, b: 148}, {r: 133, g: 160, b: 210}, {r: 240, g: 240, b: 240}, {r: 199, g: 155, b: 125}]; // Make sure this number tracks the number of reserved entries in the // palette. var numReservedColorIds = 5; function brighten(c) { var k; if (c.r >= 240 && c.g >= 240 && c.b >= 240) k = -0.20; else k = 0.45; return {r: Math.min(255, c.r + Math.floor(c.r * k)), g: Math.min(255, c.g + Math.floor(c.g * k)), b: Math.min(255, c.b + Math.floor(c.b * k))}; } function colorToRGBString(c) { return 'rgb(' + c.r + ',' + c.g + ',' + c.b + ')'; } function colorToRGBAString(c, a) { return 'rgba(' + c.r + ',' + c.g + ',' + c.b + ',' + a + ')'; } /** * The number of color IDs that getStringColorId can choose from. */ var numRegularColorIds = paletteBase.length - numReservedColorIds; var highlightIdBoost = paletteBase.length; var paletteRaw = paletteBase.concat(paletteBase.map(brighten)); var palette = paletteRaw.map(colorToRGBString); /** * Computes a simplistic hashcode of the provide name. Used to chose colors * for slices. * @param {string} name The string to hash. */ function getStringHash(name) { var hash = 0; for (var i = 0; i < name.length; ++i) hash = (hash + 37 * hash + 11 * name.charCodeAt(i)) % 0xFFFFFFFF; return hash; } /** * Gets the color palette. */ function getColorPalette() { return palette; } /** * @return {Number} The value to add to a color ID to get its highlighted * colro ID. E.g. 7 + getPaletteHighlightIdBoost() yields a brightened from * of 7's base color. */ function getColorPaletteHighlightIdBoost() { return highlightIdBoost; } /** * @param {String} name The color name. * @return {Number} The color ID for the given color name. */ function getColorIdByName(name) { if (name == 'iowait') return numRegularColorIds; if (name == 'running') return numRegularColorIds + 1; if (name == 'runnable') return numRegularColorIds + 2; if (name == 'sleeping') return numRegularColorIds + 3; if (name == 'UNKNOWN') return numRegularColorIds + 4; throw new Error('Unrecognized color ') + name; } // Previously computed string color IDs. They are based on a stable hash, so // it is safe to save them throughout the program time. var stringColorIdCache = {}; /** * @return {Number} A color ID that is stably associated to the provided via * the getStringHash method. The color ID will be chosen from the regular * ID space only, e.g. no reserved ID will be used. */ function getStringColorId(string) { if (stringColorIdCache[string] === undefined) { var hash = getStringHash(string); stringColorIdCache[string] = hash % numRegularColorIds; } return stringColorIdCache[string]; } /** * Provides methods to get view values for events. */ var EventPresenter = { getAlpha_: function(event) { if (event.selectionState === SelectionState.DIMMED) return 0.3; return 1.0; }, getColorIdOffset_: function(event) { if (event.selectionState === SelectionState.SELECTED) return highlightIdBoost; return 0; }, getTextColor: function(event) { if (event.selectionState === SelectionState.DIMMED) return 'rgb(60,60,60)'; return 'rgb(0,0,0)'; }, getSliceColorId: function(slice) { return slice.colorId + this.getColorIdOffset_(slice); }, getSliceAlpha: function(slice, async) { var alpha = this.getAlpha_(slice); if (async) alpha *= 0.3; return alpha; }, getInstantSliceColor: function(instant) { var colorId = instant.colorId + this.getColorIdOffset_(instant); return colorToRGBAString(paletteRaw[colorId], this.getAlpha_(instant)); }, getObjectInstanceColor: function(instance) { var colorId = instance.colorId + this.getColorIdOffset_(instance); return colorToRGBAString(paletteRaw[colorId], 0.25); }, getObjectSnapshotColor: function(snapshot) { var colorId = snapshot.objectInstance.colorId + this.getColorIdOffset_(snapshot); return palette[colorId]; }, getCounterSeriesColor: function(colorId, selectionState) { return colorToRGBAString( paletteRaw[colorId], this.getAlpha_({selectionState: selectionState})); }, getBarSnapshotColor: function(snapshot, offset) { var colorId = (snapshot.objectInstance.colorId + offset) % numRegularColorIds; colorId += this.getColorIdOffset_(snapshot); return colorToRGBAString(paletteRaw[colorId], this.getAlpha_(snapshot)); } }; return { getColorPalette: getColorPalette, getColorPaletteHighlightIdBoost: getColorPaletteHighlightIdBoost, getColorIdByName: getColorIdByName, getStringHash: getStringHash, getStringColorId: getStringColorId, EventPresenter: EventPresenter }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the SliceGroup class. */ base.require('base.range'); base.require('tracing.trace_model.slice'); base.require('tracing.color_scheme'); base.require('tracing.filter'); base.exportTo('tracing.trace_model', function() { var Slice = tracing.trace_model.Slice; /** * A group of Slices, plus code to create them from B/E events, as * well as arrange them into subRows. * * Do not mutate the slices array directly. Modify it only by * SliceGroup mutation methods. * * @constructor * @param {function(new:Slice, category, title, colorId, start, args)=} * opt_sliceConstructor The constructor to use when creating slices. */ function SliceGroup(opt_sliceConstructor) { var sliceConstructor = opt_sliceConstructor || Slice; this.sliceConstructor = sliceConstructor; this.openPartialSlices_ = []; this.slices = []; this.bounds = new base.Range(); this.topLevelSlices = []; } SliceGroup.prototype = { __proto__: Object.prototype, /** * @return {Number} The number of slices in this group. */ get length() { return this.slices.length; }, /** * Helper function that pushes the provided slice onto the slices array. * @param {Slice} slice The slice to be added to the slices array. */ pushSlice: function(slice) { this.slices.push(slice); return slice; }, /** * Helper function that pushes the provided slice onto the slices array. * @param {Array.} slices An array of slices to be added. */ pushSlices: function(slices) { this.slices.push.apply(this.slices, slices); }, /** * Push an instant event into the slice list. * @param {tracing.trace_model.InstantEvent} instantEvent The instantEvent. */ pushInstantEvent: function(instantEvent) { this.slices.push(instantEvent); }, /** * Opens a new slice in the group's slices. * * Calls to beginSlice and * endSlice must be made with non-monotonically-decreasing timestamps. * * @param {String} category Category name of the slice to add. * @param {String} title Title of the slice to add. * @param {Number} ts The timetsamp of the slice, in milliseconds. * @param {Object.=} opt_args Arguments associated with * the slice. */ beginSlice: function(category, title, ts, opt_args) { if (this.openPartialSlices_.length) { var prevSlice = this.openPartialSlices_[ this.openPartialSlices_.length - 1]; if (ts < prevSlice.start) throw new Error('Slices must be added in increasing timestamp order'); } var colorId = tracing.getStringColorId(title); var slice = new this.sliceConstructor(category, title, colorId, ts, opt_args ? opt_args : {}); this.openPartialSlices_.push(slice); return slice; }, isTimestampValidForBeginOrEnd: function(ts) { if (!this.openPartialSlices_.length) return true; var top = this.openPartialSlices_[this.openPartialSlices_.length - 1]; return ts >= top.start; }, /** * @return {Number} The number of beginSlices for which an endSlice has not * been issued. */ get openSliceCount() { return this.openPartialSlices_.length; }, get mostRecentlyOpenedPartialSlice() { if (!this.openPartialSlices_.length) return undefined; return this.openPartialSlices_[this.openPartialSlices_.length - 1]; }, /** * Ends the last begun slice in this group and pushes it onto the slice * array. * * @param {Number} ts Timestamp when the slice ended. * @return {Slice} slice. */ endSlice: function(ts) { if (!this.openSliceCount) throw new Error('endSlice called without an open slice'); var slice = this.openPartialSlices_[this.openSliceCount - 1]; this.openPartialSlices_.splice(this.openSliceCount - 1, 1); if (ts < slice.start) throw new Error('Slice ' + slice.title + ' end time is before its start.'); slice.duration = ts - slice.start; this.pushSlice(slice); return slice; }, /** * Push a complete event as a Slice into the slice list. * The timestamp can be in any order. * * @param {String} category Category name of the slice to add. * @param {String} title Title of the slice to add. * @param {Number} ts The timetsamp of the slice, in milliseconds. * @param {Number} duration The duration of the slice, in milliseconds. * @param {Object.=} opt_args Arguments associated with * the slice. */ pushCompleteSlice: function(category, title, ts, duration, opt_args) { var colorId = tracing.getStringColorId(title); var slice = new this.sliceConstructor(category, title, colorId, ts, opt_args ? opt_args : {}, duration); this.pushSlice(slice); return slice; }, /** * Closes any open slices. * @param {Number=} opt_maxTimestamp The end time to use for the closed * slices. If not provided, * the max timestamp for this slice is provided. */ autoCloseOpenSlices: function(opt_maxTimestamp) { if (!opt_maxTimestamp) { this.updateBounds(); opt_maxTimestamp = this.bounds.max; } while (this.openSliceCount > 0) { var slice = this.endSlice(opt_maxTimestamp); slice.didNotFinish = true; } }, /** * Shifts all the timestamps inside this group forward by the amount * specified. */ shiftTimestampsForward: function(amount) { for (var sI = 0; sI < this.slices.length; sI++) { var slice = this.slices[sI]; slice.start = (slice.start + amount); } for (var sI = 0; sI < this.openPartialSlices_.length; sI++) { var slice = this.openPartialSlices_[i]; slice.start = (slice.start + amount); } }, /** * Updates the bounds for this group based on the slices it contains. */ updateBounds: function() { this.bounds.reset(); for (var i = 0; i < this.slices.length; i++) { this.bounds.addValue(this.slices[i].start); this.bounds.addValue(this.slices[i].end); } if (this.openPartialSlices_.length) { this.bounds.addValue(this.openPartialSlices_[0].start); this.bounds.addValue( this.openPartialSlices_[this.openPartialSlices_.length - 1].start); } }, copySlice: function(slice) { var newSlice = new this.sliceConstructor(slice.category, slice.title, slice.colorId, slice.start, slice.args, slice.duration); newSlice.didNotFinish = slice.didNotFinish; return newSlice; }, iterateAllEvents: function(callback) { this.slices.forEach(callback); this.openPartialSlices_.forEach(callback); }, /** * Construct subSlices for this group. * Populate the group topLevelSlices, parent slices get a subSlices[] * and a selfTime, child slices get a parentSlice reference. */ createSubSlices: function() { function addSliceIfBounds(root, child) { // Because we know that the start time of child is >= the start time // of all other slices seen so far, we can just check the last slice // of each row for bounding. if (child.start >= root.start && child.end <= root.end) { if (root.subSlices && root.subSlices.length > 0) { if (addSliceIfBounds(root.subSlices[root.subSlices.length - 1], child)) return true; } if (!root.selfTime) root.selfTime = root.duration; child.parentSlice = root; if (!root.subSlices) root.subSlices = []; root.subSlices.push(child); root.selfTime -= child.duration; return true; } return false; } if (!this.slices.length) return; var ops = []; for (var i = 0; i < this.slices.length; i++) { if (this.slices[i].subSlices) this.slices[i].subSlices.splice(0, this.slices[i].subSlices.length); ops.push(i); } var groupSlices = this.slices; ops.sort(function(ix, iy) { var x = groupSlices[ix]; var y = groupSlices[iy]; if (x.start != y.start) return x.start - y.start; // Elements get inserted into the slices array in order of when the // slices end. Because slices must be properly nested, we break // start-time ties by assuming that the elements appearing earlier // in the slices array (and thus ending earlier) start later. return iy - ix; }); var rootSlice = this.slices[ops[0]]; this.topLevelSlices = []; this.topLevelSlices.push(rootSlice); for (var i = 1; i < ops.length; i++) { var slice = this.slices[ops[i]]; if (!addSliceIfBounds(rootSlice, slice)) { rootSlice = slice; this.topLevelSlices.push(rootSlice); } } } }; /** * Merge two slice groups. * * If the two groups do not nest properly some of the slices of groupB will * be split to accomodate the improper nesting. This is done to accomodate * combined kernel and userland call stacks on Android. Because userland * tracing is done by writing to the trace_marker file, the kernel calls * that get invoked as part of that write may not be properly nested with * the userland call trace. For example the following sequence may occur: * * kernel enter sys_write (the write to trace_marker) * user enter some_function * kernel exit sys_write * ... * kernel enter sys_write (the write to trace_marker) * user exit some_function * kernel exit sys_write * * This is handled by splitting the sys_write call into two slices as * follows: * * | sys_write | some_function | sys_write (cont.) | * | sys_write (cont.) | | sys_write | * * The colorId of both parts of the split slices are kept the same, and the * " (cont.)" suffix is appended to the later parts of a split slice. * * The two input SliceGroups are not modified by this, and the merged * SliceGroup will contain a copy of each of the input groups' slices (those * copies may be split). */ SliceGroup.merge = function(groupA, groupB) { // This is implemented by traversing the two slice groups in reverse // order. The slices in each group are sorted by ascending end-time, so // we must do the traversal from back to front in order to maintain the // sorting. // // We traverse the two groups simultaneously, merging as we go. At each // iteration we choose the group from which to take the next slice based // on which group's next slice has the greater end-time. During this // traversal we maintain a stack of currently "open" slices for each input // group. A slice is considered "open" from the time it gets reached in // our input group traversal to the time we reach an slice in this // traversal with an end-time before the start time of the "open" slice. // // Each time a slice from groupA is opened or closed (events corresponding // to the end-time and start-time of the input slice, respectively) we // split all of the currently open slices from groupB. if (groupA.openPartialSlices_.length > 0) { throw new Error('groupA has open partial slices'); } if (groupB.openPartialSlices_.length > 0) { throw new Error('groupB has open partial slices'); } var result = new SliceGroup(); var slicesA = groupA.slices; var slicesB = groupB.slices; var idxA = slicesA.length - 1; var idxB = slicesB.length - 1; var openA = []; var openB = []; var splitOpenSlices = function(when) { for (var i = 0; i < openB.length; i++) { var oldSlice = openB[i]; var oldEnd = oldSlice.end; if (when < oldSlice.start || oldEnd < when) { throw new Error('slice should not be split'); } var newSlice = result.copySlice(oldSlice); oldSlice.start = when; oldSlice.duration = oldEnd - when; oldSlice.title += ' (cont.)'; newSlice.duration = when - newSlice.start; openB[i] = newSlice; result.pushSlice(newSlice); } }; var closeOpenSlices = function(upTo) { while (openA.length > 0 || openB.length > 0) { var nextA = openA[openA.length - 1]; var nextB = openB[openB.length - 1]; var startA = nextA && nextA.start; var startB = nextB && nextB.start; if ((startA === undefined || startA < upTo) && (startB === undefined || startB < upTo)) { return; } if (startB === undefined || startA > startB) { splitOpenSlices(startA); openA.pop(); } else { openB.pop(); } } }; while (idxA >= 0 || idxB >= 0) { var sA = slicesA[idxA]; var sB = slicesB[idxB]; var nextSlice, isFromB; if (sA === undefined || (sB !== undefined && sA.end < sB.end)) { nextSlice = result.copySlice(sB); isFromB = true; idxB--; } else { nextSlice = result.copySlice(sA); isFromB = false; idxA--; } closeOpenSlices(nextSlice.end); result.pushSlice(nextSlice); if (isFromB) { openB.push(nextSlice); } else { splitOpenSlices(nextSlice.end); openA.push(nextSlice); } } closeOpenSlices(); result.slices.reverse(); return result; }; return { SliceGroup: SliceGroup }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the Thread class. */ base.require('base.guid'); base.require('base.range'); base.require('tracing.trace_model.slice'); base.require('tracing.trace_model.slice_group'); base.require('tracing.trace_model.async_slice_group'); base.require('tracing.trace_model.sample'); base.exportTo('tracing.trace_model', function() { var Slice = tracing.trace_model.Slice; var SliceGroup = tracing.trace_model.SliceGroup; var AsyncSlice = tracing.trace_model.AsyncSlice; var AsyncSliceGroup = tracing.trace_model.AsyncSliceGroup; /** * A ThreadSlice represents an interval of time on a thread resource * with associated nestinged slice information. * * ThreadSlices are typically associated with a specific trace event pair on a * specific thread. * For example, * TRACE_EVENT_BEGIN1("x","myArg", 7) at time=0.1ms * TRACE_EVENT_END0() at time=0.3ms * This results in a single slice from 0.1 with duration 0.2 on a * specific thread. * * @constructor */ function ThreadSlice(cat, title, colorId, start, args, opt_duration) { Slice.call(this, cat, title, colorId, start, args, opt_duration); // Do not modify this directly. // subSlices is configured by SliceGroup.rebuildSubRows_. this.subSlices = []; } ThreadSlice.prototype = { __proto__: Slice.prototype }; /** * A Thread stores all the trace events collected for a particular * thread. We organize the synchronous slices on a thread by "subrows," where * subrow 0 has all the root slices, subrow 1 those nested 1 deep, and so on. * The asynchronous slices are stored in an AsyncSliceGroup object. * * The slices stored on a Thread should be instances of * ThreadSlice. * * @constructor */ function Thread(parent, tid) { this.guid_ = base.GUID.allocate(); if (!parent) throw new Error('Parent must be provided.'); this.parent = parent; this.sortIndex = 0; this.tid = tid; this.sliceGroup = new SliceGroup(ThreadSlice); this.timeSlices = undefined; this.samples_ = []; this.kernelSliceGroup = new SliceGroup(); this.asyncSliceGroup = new AsyncSliceGroup(); this.bounds = new base.Range(); this.ephemeralSettings = {}; } Thread.prototype = { /* * @return {Number} A globally unique identifier for this counter. */ get guid() { return this.guid_; }, compareTo: function(that) { return Thread.compare(this, that); }, toJSON: function() { var obj = new Object(); var keys = Object.keys(this); for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (typeof this[key] == 'function') continue; if (key == 'parent') { obj[key] = this[key].guid; continue; } obj[key] = this[key]; } return obj; }, /** * Adds a new sample in the thread's samples. * * Calls to addSample must be made with non-monotonically-decreasing * timestamps. * * @param {String} category Category of the sample to add. * @param {String} title Title of the sample to add. * @param {Number} ts The timetsamp of the sample, in milliseconds. * @param {Object.=} opt_args Arguments associated with * the sample. */ addSample: function(category, title, ts, opt_args) { if (this.samples_.length) { var lastSample = this.samples_[this.samples_.length - 1]; if (ts < lastSample.start) { throw new Error('Samples must be added in increasing timestamp order.'); } } var colorId = tracing.getStringColorId(title); var sample = new tracing.trace_model.Sample(category, title, colorId, ts, opt_args ? opt_args : {}); this.samples_.push(sample); return sample; }, /** * Returns the array of samples added to this thread. If no samples * have been added, an empty array is returned. * * @return {Array} array of samples. */ get samples() { return this.samples_; }, /** * Name of the thread, if present. */ name: undefined, /** * Shifts all the timestamps inside this thread forward by the amount * specified. */ shiftTimestampsForward: function(amount) { this.sliceGroup.shiftTimestampsForward(amount); if (this.timeSlices) { for (var i = 0; i < this.timeSlices.length; i++) { var slice = this.timeSlices[i]; slice.start += amount; } } if (this.samples_.length) { for (var i = 0; i < this.samples_.length; i++) { var sample = this.samples_[i]; sample.start += amount; } } this.kernelSliceGroup.shiftTimestampsForward(amount); this.asyncSliceGroup.shiftTimestampsForward(amount); }, /** * Determins whether this thread is empty. If true, it usually implies * that it should be pruned from the model. */ get isEmpty() { if (this.sliceGroup.length) return false; if (this.sliceGroup.openSliceCount) return false; if (this.timeSlices && this.timeSlices.length) return false; if (this.kernelSliceGroup.length) return false; if (this.asyncSliceGroup.length) return false; if (this.samples_.length) return false; return true; }, /** * Updates the bounds based on the * current objects associated with the thread. */ updateBounds: function() { this.bounds.reset(); this.sliceGroup.updateBounds(); this.bounds.addRange(this.sliceGroup.bounds); this.kernelSliceGroup.updateBounds(); this.bounds.addRange(this.kernelSliceGroup.bounds); this.asyncSliceGroup.updateBounds(); this.bounds.addRange(this.asyncSliceGroup.bounds); if (this.timeSlices && this.timeSlices.length) { this.bounds.addValue(this.timeSlices[0].start); this.bounds.addValue( this.timeSlices[this.timeSlices.length - 1].end); } if (this.samples_.length) { this.bounds.addValue(this.samples_[0].start); this.bounds.addValue( this.samples_[this.samples_.length - 1].end); } }, addCategoriesToDict: function(categoriesDict) { for (var i = 0; i < this.sliceGroup.length; i++) categoriesDict[this.sliceGroup.slices[i].category] = true; for (var i = 0; i < this.kernelSliceGroup.length; i++) categoriesDict[this.kernelSliceGroup.slices[i].category] = true; for (var i = 0; i < this.asyncSliceGroup.length; i++) categoriesDict[this.asyncSliceGroup.slices[i].category] = true; for (var i = 0; i < this.samples_.length; i++) categoriesDict[this.samples_[i].category] = true; }, autoCloseOpenSlices: function(opt_maxTimestamp) { this.sliceGroup.autoCloseOpenSlices(opt_maxTimestamp); this.kernelSliceGroup.autoCloseOpenSlices(opt_maxTimestamp); }, mergeKernelWithUserland: function() { if (this.kernelSliceGroup.length > 0) { var newSlices = SliceGroup.merge( this.sliceGroup, this.kernelSliceGroup); this.sliceGroup.slices = newSlices.slices; this.kernelSliceGroup = new SliceGroup(); this.updateBounds(); } }, createSubSlices: function() { this.sliceGroup.createSubSlices(); }, /** * @return {String} A user-friendly name for this thread. */ get userFriendlyName() { return this.name || this.tid; }, /** * @return {String} User friendly details about this thread. */ get userFriendlyDetails() { return 'tid: ' + this.tid + (this.name ? ', name: ' + this.name : ''); }, getSettingsKey: function() { if (!this.name) return undefined; var parentKey = this.parent.getSettingsKey(); if (!parentKey) return undefined; return parentKey + '.' + this.name; }, /* * Returns the index of the slice in the timeSlices array, or undefined. */ indexOfTimeSlice: function(timeSlice) { var i = base.findLowIndexInSortedArray( this.timeSlices, function(slice) { return slice.start; }, timeSlice.start); if (this.timeSlices[i] !== timeSlice) return undefined; return i; }, iterateAllEvents: function(callback) { this.sliceGroup.iterateAllEvents(callback); this.kernelSliceGroup.iterateAllEvents(callback); this.asyncSliceGroup.iterateAllEvents(callback); if (this.timeSlices && this.timeSlices.length) this.timeSlices.forEach(callback); this.samples_.forEach(callback); } }; /** * Comparison between threads that orders first by parent.compareTo, * then by names, then by tid. */ Thread.compare = function(x, y) { var tmp = x.parent.compareTo(y.parent); if (tmp) return tmp; tmp = x.sortIndex - y.sortIndex; if (tmp) return tmp; tmp = base.comparePossiblyUndefinedValues( x.name, y.name, function(x, y) { return x.localeCompare(y); }); if (tmp) return tmp; return x.tid - y.tid; }; return { ThreadSlice: ThreadSlice, Thread: Thread }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the Settings object. */ base.exportTo('base', function() { var storage_ = localStorage; /** * Settings is a simple wrapper around local storage, to make it easier * to test classes that have settings. * * May be called as new base.Settings() or simply base.Settings() * @constructor */ function Settings() { return Settings; }; /** * Get the setting with the given name. * * @param {string} key The name of the setting. * @param {string=} opt_default The default value to return if not set. * @param {string=} opt_namespace If set, the setting name will be prefixed * with this namespace, e.g. "categories.settingName". This is useful for * a set of related settings. */ Settings.get = function(key, opt_default, opt_namespace) { key = Settings.namespace_(key, opt_namespace); var rawVal = storage_.getItem(key); if (rawVal === null || rawVal === undefined) return opt_default; // Old settings versions used to stringify objects instead of putting them // into JSON. If those are encountered, parse will fail. In that case, // "upgrade" the setting to the default value. try { return JSON.parse(rawVal).value; } catch (e) { storage_.removeItem(Settings.namespace_(key, opt_namespace)); return opt_default; } }, /** * Set the setting with the given name to the given value. * * @param {string} key The name of the setting. * @param {string} value The value of the setting. * @param {string=} opt_namespace If set, the setting name will be prefixed * with this namespace, e.g. "categories.settingName". This is useful for * a set of related settings. */ Settings.set = function(key, value, opt_namespace) { if (value === undefined) throw new Error('Settings.set: value must not be undefined'); var v = JSON.stringify({value: value}); storage_.setItem(Settings.namespace_(key, opt_namespace), v); }, /** * Return a list of all the keys, or all the keys in the given namespace * if one is provided. * * @param {string=} opt_namespace If set, only return settings which * begin with this prefix. */ Settings.keys = function(opt_namespace) { var result = []; opt_namespace = opt_namespace || ''; for (var i = 0; i < storage_.length; i++) { var key = storage_.key(i); if (Settings.isnamespaced_(key, opt_namespace)) result.push(Settings.unnamespace_(key, opt_namespace)); } return result; }, Settings.isnamespaced_ = function(key, opt_namespace) { return key.indexOf(Settings.normalize_(opt_namespace)) == 0; }, Settings.namespace_ = function(key, opt_namespace) { return Settings.normalize_(opt_namespace) + key; }, Settings.unnamespace_ = function(key, opt_namespace) { return key.replace(Settings.normalize_(opt_namespace), ''); }, /** * All settings are prefixed with a global namespace to avoid collisions. * Settings may also be namespaced with an additional prefix passed into * the get, set, and keys methods in order to group related settings. * This method makes sure the two namespaces are always set properly. */ Settings.normalize_ = function(opt_namespace) { return Settings.NAMESPACE + (opt_namespace ? opt_namespace + '.' : ''); } Settings.setAlternativeStorageInstance = function(instance) { storage_ = instance; } Settings.getAlternativeStorageInstance = function() { if (storage_ === localStorage) return undefined; return storage_; } Settings.NAMESPACE = 'trace-viewer'; return { Settings: Settings }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.settings'); base.exportTo('tracing', function() { var Settings = base.Settings; /** * A way to persist settings specific to parts of a trace model. * * This object should not be persisted because it builds up internal data * structures that map model objects to settings keys. It should thus be * created for the druation of whatever interaction(s) you're going to do with * model settings, and then discarded. * * This system works on a notion of an object key: for an object's key, it * considers all the other keys in the model. If it is unique, then the key is * persisted to base.Settings. However, if it is not unique, then the * setting is stored on the object itself. Thus, objects with unique keys will * be persisted across page reloads, whereas objects with nonunique keys will * not. */ function TraceModelSettings(model) { this.model = model; this.objectsByKey_ = []; this.nonuniqueKeys_ = []; this.buildObjectsByKeyMap_(); this.removeNonuniqueKeysFromSettings_(); } TraceModelSettings.prototype = { buildObjectsByKeyMap_: function() { var objects = [this.model.kernel]; objects.push.apply(objects, base.dictionaryValues(this.model.processes)); objects.push.apply(objects, this.model.getAllThreads()); var objectsByKey = {}; var NONUNIQUE_KEY = 'nonuniqueKey'; for (var i = 0; i < objects.length; i++) { var object = objects[i]; var objectKey = object.getSettingsKey(); if (!objectKey) continue; if (objectsByKey[objectKey] === undefined) { objectsByKey[objectKey] = object; continue; } objectsByKey[objectKey] = NONUNIQUE_KEY; } var nonuniqueKeys = {}; base.dictionaryKeys(objectsByKey).forEach(function(objectKey) { if (objectsByKey[objectKey] !== NONUNIQUE_KEY) return; delete objectsByKey[objectKey]; nonuniqueKeys[objectKey] = true; }); this.nonuniqueKeys = nonuniqueKeys; this.objectsByKey_ = objectsByKey; }, removeNonuniqueKeysFromSettings_: function() { var settings = Settings.get('trace_model_settings', {}); var settingsChanged = false; base.dictionaryKeys(settings).forEach(function(objectKey) { if (!this.nonuniqueKeys[objectKey]) return; settingsChanged = true; delete settings[objectKey]; }, this); if (settingsChanged) Settings.set('trace_model_settings', settings); }, hasUniqueSettingKey: function(object) { var objectKey = object.getSettingsKey(); if (!objectKey) return false; return this.objectsByKey_[objectKey] !== undefined; }, getSettingFor: function(object, objectLevelKey, defaultValue) { var objectKey = object.getSettingsKey(); if (!objectKey || !this.objectsByKey_[objectKey]) { var ephemeralValue = object.ephemeralSettings[objectLevelKey]; if (ephemeralValue !== undefined) return ephemeralValue; return defaultValue; } var settings = Settings.get('trace_model_settings', {}); if (!settings[objectKey]) settings[objectKey] = {}; var value = settings[objectKey][objectLevelKey]; if (value !== undefined) return value; return defaultValue; }, setSettingFor: function(object, objectLevelKey, value) { var objectKey = object.getSettingsKey(); if (!objectKey || !this.objectsByKey_[objectKey]) { object.ephemeralSettings[objectLevelKey] = value; return; } var settings = Settings.get('trace_model_settings', {}); if (!settings[objectKey]) settings[objectKey] = {}; settings[objectKey][objectLevelKey] = value; Settings.set('trace_model_settings', settings); } }; return { TraceModelSettings: TraceModelSettings }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the ProcessBase class. */ base.require('base.guid'); base.require('base.range'); base.require('tracing.trace_model.counter'); base.require('tracing.trace_model.object_collection'); base.require('tracing.trace_model.thread'); base.require('tracing.trace_model_settings'); base.exportTo('tracing.trace_model', function() { var Thread = tracing.trace_model.Thread; var Counter = tracing.trace_model.Counter; /** * The ProcessBase is an partial base class, upon which Kernel * and Process are built. * * @constructor */ function ProcessBase(model) { if (!model) throw new Error('Must provide a model'); this.guid_ = base.GUID.allocate(); this.model = model; this.threads = {}; this.counters = {}; this.objects = new tracing.trace_model.ObjectCollection(this); this.bounds = new base.Range(); this.sortIndex = 0; this.ephemeralSettings = {}; }; ProcessBase.compare = function(x, y) { return x.sortIndex - y.sortIndex; }; ProcessBase.prototype = { /* * @return {Number} A globally unique identifier for this counter. */ get guid() { return this.guid_; }, /** * Gets the number of threads in this process. */ get numThreads() { var n = 0; for (var p in this.threads) { n++; } return n; }, toJSON: function() { var obj = new Object(); var keys = Object.keys(this); for (var i = 0; i < keys.length; i++) { var key = keys[i]; if (typeof this[key] == 'function') continue; if (key == 'model') continue; obj[key] = this[key]; } return obj; }, /** * Shifts all the timestamps inside this process forward by the amount * specified. */ shiftTimestampsForward: function(amount) { for (var tid in this.threads) this.threads[tid].shiftTimestampsForward(amount); for (var id in this.counters) this.counters[id].shiftTimestampsForward(amount); this.objects.shiftTimestampsForward(amount); }, /** * Closes any open slices. */ autoCloseOpenSlices: function(opt_maxTimestamp) { for (var tid in this.threads) { var thread = this.threads[tid]; thread.autoCloseOpenSlices(opt_maxTimestamp); } }, autoDeleteObjects: function(maxTimestamp) { this.objects.autoDeleteObjects(maxTimestamp); }, /** * Called by the model after finalizing imports, * but before joining refs. */ preInitializeObjects: function() { this.objects.preInitializeAllObjects(); }, /** * Called by the model after joining refs. */ initializeObjects: function() { this.objects.initializeAllObjects(); }, /** * Merge slices from the kernel with those from userland for each thread. */ mergeKernelWithUserland: function() { for (var tid in this.threads) { var thread = this.threads[tid]; thread.mergeKernelWithUserland(); } }, updateBounds: function() { this.bounds.reset(); for (var tid in this.threads) { this.threads[tid].updateBounds(); this.bounds.addRange(this.threads[tid].bounds); } for (var id in this.counters) { this.counters[id].updateBounds(); this.bounds.addRange(this.counters[id].bounds); } this.objects.updateBounds(); this.bounds.addRange(this.objects.bounds); }, addCategoriesToDict: function(categoriesDict) { for (var tid in this.threads) this.threads[tid].addCategoriesToDict(categoriesDict); for (var id in this.counters) categoriesDict[this.counters[id].category] = true; this.objects.addCategoriesToDict(categoriesDict); }, /** * @param {String} The name of the thread to find. * @return {Array} An array of all the matched threads. */ findAllThreadsNamed: function(name) { var namedThreads = []; for (var tid in this.threads) { var thread = this.threads[tid]; if (thread.name == name) namedThreads.push(thread); } return namedThreads; }, /** * Removes threads from the process that are fully empty. */ pruneEmptyContainers: function() { var threadsToKeep = {}; for (var tid in this.threads) { var thread = this.threads[tid]; if (!thread.isEmpty) threadsToKeep[tid] = thread; } this.threads = threadsToKeep; }, /** * @return {TimlineThread} The thread identified by tid on this process, * creating it if it doesn't exist. */ getOrCreateThread: function(tid) { if (!this.threads[tid]) this.threads[tid] = new Thread(this, tid); return this.threads[tid]; }, /** * @return {TimlineCounter} The counter on this process named 'name', * creating it if it doesn't exist. */ getOrCreateCounter: function(cat, name) { var id = cat + '.' + name; if (!this.counters[id]) this.counters[id] = new Counter(this, id, cat, name); return this.counters[id]; }, getSettingsKey: function() { throw new Error('Not implemented'); }, iterateAllEvents: function(callback) { for (var tid in this.threads) this.threads[tid].iterateAllEvents(callback); for (var id in this.counters) this.counters[id].iterateAllEvents(callback); this.objects.iterateAllEvents(callback); } }; return { ProcessBase: ProcessBase }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the Process class. */ base.require('tracing.trace_model.cpu'); base.require('tracing.trace_model.process_base'); base.exportTo('tracing.trace_model', function() { var Cpu = tracing.trace_model.Cpu; var ProcessBase = tracing.trace_model.ProcessBase; /** * The Kernel represents kernel-level objects in the * model. * @constructor */ function Kernel(model) { if (model === undefined) throw new Error('model must be provided'); ProcessBase.call(this, model); this.cpus = {}; }; /** * Comparison between kernels is pretty meaningless. */ Kernel.compare = function(x, y) { return 0; }; Kernel.prototype = { __proto__: ProcessBase.prototype, compareTo: function(that) { return Kernel.compare(this, that); }, get userFriendlyName() { return 'Kernel'; }, get userFriendlyDetails() { return 'Kernel'; }, /** * @return {Cpu} Gets a specific Cpu or creates one if * it does not exist. */ getOrCreateCpu: function(cpuNumber) { if (!this.cpus[cpuNumber]) this.cpus[cpuNumber] = new Cpu(cpuNumber); return this.cpus[cpuNumber]; }, shiftTimestampsForward: function(amount) { ProcessBase.prototype.shiftTimestampsForward.call(this); for (var cpuNumber in this.cpus) this.cpus[cpuNumber].shiftTimestampsForward(amount); }, updateBounds: function() { ProcessBase.prototype.updateBounds.call(this); for (var cpuNumber in this.cpus) { var cpu = this.cpus[cpuNumber]; cpu.updateBounds(); this.bounds.addRange(cpu.bounds); } }, addCategoriesToDict: function(categoriesDict) { ProcessBase.prototype.addCategoriesToDict.call(this, categoriesDict); for (var cpuNumber in this.cpus) this.cpus[cpuNumber].addCategoriesToDict(categoriesDict); }, getSettingsKey: function() { return 'kernel'; }, iterateAllEvents: function(callback) { for (var cpuNumber in this.cpus) this.cpus[cpuNumber].iterateAllEvents(callback); ProcessBase.prototype.iterateAllEvents.call(this, callback); } }; return { Kernel: Kernel }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Provides the Process class. */ base.require('tracing.trace_model.process_base'); base.exportTo('tracing.trace_model', function() { var ProcessBase = tracing.trace_model.ProcessBase; /** * The Process represents a single userland process in the * trace. * @constructor */ function Process(model, pid) { if (model === undefined) throw new Error('model must be provided'); if (pid === undefined) throw new Error('pid must be provided'); tracing.trace_model.ProcessBase.call(this, model); this.pid = pid; this.name = undefined; this.labels = []; this.instantEvents = []; }; /** * Comparison between processes that orders by pid. */ Process.compare = function(x, y) { var tmp = tracing.trace_model.ProcessBase.compare(x, y); if (tmp) return tmp; tmp = base.comparePossiblyUndefinedValues( x.name, y.name, function(x, y) { return x.localeCompare(y); }); if (tmp) return tmp; tmp = base.compareArrays(x.labels, y.labels, function(x, y) { return x.localeCompare(y); }); if (tmp) return tmp; return x.pid - y.pid; }; Process.prototype = { __proto__: tracing.trace_model.ProcessBase.prototype, compareTo: function(that) { return Process.compare(this, that); }, pushInstantEvent: function(instantEvent) { this.instantEvents.push(instantEvent); }, get userFriendlyName() { var res; if (this.name) res = this.name + ' (pid ' + this.pid + ')'; else res = 'Process ' + this.pid; if (this.labels.length) res += ': ' + this.labels.join(', '); return res; }, get userFriendlyDetails() { if (this.name) return this.name + ' (pid ' + this.pid + ')'; return 'pid: ' + this.pid; }, getSettingsKey: function() { if (!this.name) return undefined; if (!this.labels.length) return 'processes.' + this.name; return 'processes.' + this.name + '.' + this.labels.join('.'); }, shiftTimestampsForward: function(amount) { for (var id in this.instantEvents) this.instantEvents[id].start += amount; tracing.trace_model.ProcessBase.prototype .shiftTimestampsForward.apply(this, arguments); }, iterateAllEvents: function(callback) { this.instantEvents.forEach(callback); ProcessBase.prototype.iterateAllEvents.call(this, callback); } }; return { Process: Process }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview TraceModel is a parsed representation of the * TraceEvents obtained from base/trace_event in which the begin-end * tokens are converted into a hierarchy of processes, threads, * subrows, and slices. * * The building block of the model is a slice. A slice is roughly * equivalent to function call executing on a specific thread. As a * result, slices may have one or more subslices. * * A thread contains one or more subrows of slices. Row 0 corresponds to * the "root" slices, e.g. the topmost slices. Row 1 contains slices that * are nested 1 deep in the stack, and so on. We use these subrows to draw * nesting tasks. * */ base.require('base.range'); base.require('base.events'); base.require('base.interval_tree'); base.require('tracing.importer.importer'); base.require('tracing.importer.task'); base.require('tracing.trace_model.process'); base.require('tracing.trace_model.kernel'); base.require('tracing.filter'); base.require('ui.overlay'); base.exportTo('tracing', function() { var Importer = tracing.importer.Importer; var Process = tracing.trace_model.Process; var Kernel = tracing.trace_model.Kernel; /** * Builds a model from an array of TraceEvent objects. * @param {Object=} opt_eventData Data from a single trace to be imported into * the new model. See TraceModel.importTraces for details on how to * import multiple traces at once. * @param {bool=} opt_shiftWorldToZero Whether to shift the world to zero. * Defaults to true. * @constructor */ function TraceModel(opt_eventData, opt_shiftWorldToZero) { this.kernel = new Kernel(this); this.processes = {}; this.metadata = []; this.categories = []; this.bounds = new base.Range(); this.instantEvents = []; this.flowEvents = []; this.flowIntervalTree = new base.IntervalTree( function(s) { return s.start; }, function(e) { return e.start; }); this.importWarnings_ = []; this.reportedImportWarnings_ = {}; if (opt_eventData) this.importTraces([opt_eventData], opt_shiftWorldToZero); } TraceModel.importerConstructors_ = []; /** * Registers an importer. All registered importers are considered * when processing an import request. * * @param {Function} importerConstructor The importer's constructor function. */ TraceModel.registerImporter = function(importerConstructor) { TraceModel.importerConstructors_.push(importerConstructor); }; TraceModel.prototype = { __proto__: base.EventTarget.prototype, get numProcesses() { var n = 0; for (var p in this.processes) n++; return n; }, /** * @return {Process} Gets a TimlineProcess for a specified pid or * creates one if it does not exist. */ getOrCreateProcess: function(pid) { if (!this.processes[pid]) this.processes[pid] = new Process(this, pid); return this.processes[pid]; }, pushInstantEvent: function(instantEvent) { this.instantEvents.push(instantEvent); }, /** * Generates the set of categories from the slices and counters. */ updateCategories_: function() { var categoriesDict = {}; this.kernel.addCategoriesToDict(categoriesDict); for (var pid in this.processes) this.processes[pid].addCategoriesToDict(categoriesDict); this.categories = []; for (var category in categoriesDict) if (category != '') this.categories.push(category); }, updateBounds: function() { this.bounds.reset(); this.kernel.updateBounds(); this.bounds.addRange(this.kernel.bounds); for (var pid in this.processes) { this.processes[pid].updateBounds(); this.bounds.addRange(this.processes[pid].bounds); } }, shiftWorldToZero: function() { if (this.bounds.isEmpty) return; var timeBase = this.bounds.min; this.kernel.shiftTimestampsForward(-timeBase); for (var id in this.instantEvents) this.instantEvents[id].start -= timeBase; for (var pid in this.processes) this.processes[pid].shiftTimestampsForward(-timeBase); this.updateBounds(); }, getAllThreads: function() { var threads = []; for (var tid in this.kernel.threads) { threads.push(process.threads[tid]); } for (var pid in this.processes) { var process = this.processes[pid]; for (var tid in process.threads) { threads.push(process.threads[tid]); } } return threads; }, /** * @return {Array} An array of all processes in the model. */ getAllProcesses: function() { var processes = []; for (var pid in this.processes) processes.push(this.processes[pid]); return processes; }, /** * @return {Array} An array of all the counters in the model. */ getAllCounters: function() { var counters = []; counters.push.apply( counters, base.dictionaryValues(this.kernel.counters)); for (var pid in this.processes) { var process = this.processes[pid]; for (var tid in process.counters) { counters.push(process.counters[tid]); } } return counters; }, /** * @param {String} The name of the thread to find. * @return {Array} An array of all the matched threads. */ findAllThreadsNamed: function(name) { var namedThreads = []; namedThreads.push.apply( namedThreads, this.kernel.findAllThreadsNamed(name)); for (var pid in this.processes) { namedThreads.push.apply( namedThreads, this.processes[pid].findAllThreadsNamed(name)); } return namedThreads; }, createImporter_: function(eventData) { var importerConstructor; for (var i = 0; i < TraceModel.importerConstructors_.length; ++i) { if (TraceModel.importerConstructors_[i].canImport(eventData)) { importerConstructor = TraceModel.importerConstructors_[i]; break; } } if (!importerConstructor) throw new Error( 'Could not find an importer for the provided eventData.'); var importer = new importerConstructor( this, eventData); return importer; }, /** * Imports the provided traces into the model. The eventData type * is undefined and will be passed to all the importers registered * via TraceModel.registerImporter. The first importer that returns true * for canImport(events) will be used to import the events. * * The primary trace is provided via the eventData variable. If multiple * traces are to be imported, specify the first one as events, and the * remainder in the opt_additionalEventData array. * * @param {Array} traces An array of eventData to be imported. Each * eventData should correspond to a single trace file and will be handled by * a separate importer. * @param {bool=} opt_shiftWorldToZero Whether to shift the world to zero. * Defaults to true. * @param {bool=} opt_pruneEmptyContainers Whether to prune empty * containers. Defaults to true. */ importTraces: function(traces, opt_shiftWorldToZero, opt_pruneEmptyContainers) { var progressMeter = { update: function(msg) {} }; var task = this.createImportTracesTask( progressMeter, traces, opt_shiftWorldToZero, opt_pruneEmptyContainers); tracing.importer.Task.RunSynchronously(task); }, /** * Imports a trace with the usual options from importTraces, but * does so using idle callbacks, putting up an import dialog * during the import process. */ importTracesWithProgressDialog: function(traces, opt_shiftWorldToZero, opt_pruneEmptyContainers) { var overlay = ui.Overlay(); overlay.title = 'Importing...'; overlay.userCanClose = false; overlay.msgEl = document.createElement('div'); overlay.appendChild(overlay.msgEl); overlay.msgEl.style.margin = '20px'; overlay.update = function(msg) { this.msgEl.textContent = msg; } overlay.visible = true; var task = this.createImportTracesTask( overlay, traces, opt_shiftWorldToZero, opt_pruneEmptyContainers); var promise = tracing.importer.Task.RunWhenIdle(task); promise.then( function() { overlay.visible = false; }, function(err) { overlay.visible = false; }); return promise; }, /** * Creates a task that will import the provided traces into the model, * updating the progressMeter as it goes. Parameters are as defined in * importTraces. */ createImportTracesTask: function(progressMeter, traces, opt_shiftWorldToZero, opt_pruneEmptyContainers) { if (this.importing_) throw new Error('Already importing.'); if (opt_shiftWorldToZero === undefined) opt_shiftWorldToZero = true; if (opt_pruneEmptyContainers === undefined) opt_pruneEmptyContainers = true; this.importing_ = true; // Just some simple setup. It is useful to have a nop first // task so that we can set up the lastTask = lastTask.after() // pattern that follows. var importTask = new tracing.importer.Task(function() { progressMeter.update('I will now import your traces for you...'); }, this); var lastTask = importTask; var importers = []; lastTask = lastTask.after(function() { // Copy the traces array, we may mutate it. traces = traces.slice(0); progressMeter.update('Creating importers...'); // Figure out which importers to use. for (var i = 0; i < traces.length; ++i) importers.push(this.createImporter_(traces[i])); // Some traces have other traces inside them. Before doing the full // import, ask the importer if it has any subtraces, and if so, create // importers for them, also. for (var i = 0; i < importers.length; i++) { var subtraces = importers[i].extractSubtraces(); for (var j = 0; j < subtraces.length; j++) { traces.push(subtraces[j]); importers.push(this.createImporter_(subtraces[j])); } } // Sort them on priority. This ensures importing happens in a // predictable order, e.g. linux_perf_importer before // trace_event_importer. importers.sort(function(x, y) { return x.importPriority - y.importPriority; }); }, this); // Run the import. lastTask = lastTask.after(function(task) { importers.forEach(function(importer, index) { task.subTask(function() { progressMeter.update( 'Importing ' + (index + 1) + ' of ' + importers.length); importer.importEvents(index > 0); }, this); }, this); }, this); // Autoclose open slices. lastTask = lastTask.after(function() { progressMeter.update('Autoclosing open slices...'); this.updateBounds(); this.kernel.autoCloseOpenSlices(this.bounds.max); for (var pid in this.processes) this.processes[pid].autoCloseOpenSlices(this.bounds.max); }, this); // Finalize import. lastTask = lastTask.after(function(task) { importers.forEach(function(importer, index) { progressMeter.update( 'Finalizing import ' + (index + 1) + '/' + importers.length); importer.finalizeImport(); }, this); }, this); // Run preinit. lastTask = lastTask.after(function() { progressMeter.update('Initializing objects (step 1/2)...'); for (var pid in this.processes) this.processes[pid].preInitializeObjects(); }, this); // Prune empty containers. if (opt_pruneEmptyContainers) { lastTask = lastTask.after(function() { progressMeter.update('Pruning empty containers...'); this.kernel.pruneEmptyContainers(); for (var pid in this.processes) { this.processes[pid].pruneEmptyContainers(); } }, this); } // Merge kernel and userland slices on each thread. lastTask = lastTask.after(function() { progressMeter.update('Merging kernel with userland...'); for (var pid in this.processes) this.processes[pid].mergeKernelWithUserland(); }, this); lastTask = lastTask.after(function() { progressMeter.update('Computing final world bounds...'); this.updateBounds(); this.updateCategories_(); if (opt_shiftWorldToZero) this.shiftWorldToZero(); }, this); // Build the flow event interval tree. lastTask = lastTask.after(function() { progressMeter.update('Building flow event map...'); for (var i = 0; i < this.flowEvents.length; ++i) { var pair = this.flowEvents[i]; this.flowIntervalTree.insert(pair[0], pair[1]); } this.flowIntervalTree.updateHighValues(); }, this); // Join refs. lastTask = lastTask.after(function() { progressMeter.update('Joining object refs...'); for (var i = 0; i < importers.length; i++) importers[i].joinRefs(); }, this); // Delete any undeleted objects. lastTask = lastTask.after(function() { progressMeter.update('Cleaning up undeleted objects...'); for (var pid in this.processes) this.processes[pid].autoDeleteObjects(this.bounds.max); }, this); // Run initializers. lastTask = lastTask.after(function() { progressMeter.update('Initializing objects (step 2/2)...'); for (var pid in this.processes) this.processes[pid].initializeObjects(); }, this); // Cleanup. lastTask.after(function() { this.importing_ = false; }, this); return importTask; }, /** * @param {Object} data The import warning data. Data must provide two * accessors: type, message. The types are used to determine if we * should output the message, we'll only output one message of each type. * The message is the actual warning content. */ importWarning: function(data) { this.importWarnings_.push(data); // Only log each warning type once. We may want to add some kind of // flag to allow reporting all importer warnings. if (this.reportedImportWarnings_[data.type] === true) return; console.warn(data.message); this.reportedImportWarnings_[data.type] = true; }, get hasImportWarnings() { return (this.importWarnings_.length > 0); }, get importWarnings() { return this.importWarnings_; }, /** * Iterates all events in the model and calls callback on each event. * @param {function(event)} callback The callback called for every event. */ iterateAllEvents: function(callback) { this.instantEvents.forEach(callback); this.kernel.iterateAllEvents(callback); for (var pid in this.processes) this.processes[pid].iterateAllEvents(callback); } }; /** * Importer for empty strings and arrays. * @constructor */ function TraceModelEmptyImporter(events) { this.importPriority = 0; }; TraceModelEmptyImporter.canImport = function(eventData) { if (eventData instanceof Array && eventData.length == 0) return true; if (typeof(eventData) === 'string' || eventData instanceof String) { return eventData.length == 0; } return false; }; TraceModelEmptyImporter.prototype = { __proto__: Importer.prototype }; TraceModel.registerImporter(TraceModelEmptyImporter); return { TraceModel: TraceModel }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.trace_model.timed_event'); /** * @fileoverview Provides the InstantEvent class. */ base.exportTo('tracing.trace_model', function() { var InstantEventType = { GLOBAL: 1, PROCESS: 2, THREAD: 3 }; function InstantEvent(category, title, colorId, start, args) { tracing.trace_model.TimedEvent.call(this); this.category = category || ''; this.title = title; this.colorId = colorId; this.start = start; this.args = args; this.type = undefined; }; InstantEvent.prototype = { __proto__: tracing.trace_model.TimedEvent.prototype, selected: false }; function GlobalInstantEvent(category, title, colorId, start, args) { InstantEvent.apply(this, arguments); this.type = InstantEventType.GLOBAL; }; GlobalInstantEvent.prototype = { __proto__: InstantEvent.prototype }; function ProcessInstantEvent(category, title, colorId, start, args) { InstantEvent.apply(this, arguments); this.type = InstantEventType.PROCESS; }; ProcessInstantEvent.prototype = { __proto__: InstantEvent.prototype }; function ThreadInstantEvent(category, title, colorId, start, args) { InstantEvent.apply(this, arguments); this.type = InstantEventType.THREAD; }; ThreadInstantEvent.prototype = { __proto__: InstantEvent.prototype }; return { GlobalInstantEvent: GlobalInstantEvent, ProcessInstantEvent: ProcessInstantEvent, ThreadInstantEvent: ThreadInstantEvent, InstantEventType: InstantEventType, InstantEvent: InstantEvent }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Code for the viewport. */ base.require('base.events'); base.require('base.guid'); base.require('base.range'); base.require('tracing.trace_model.instant_event'); base.require('tracing.trace_model'); base.exportTo('tracing', function() { var EVENT_TYPES = [ { constructor: tracing.trace_model.Slice, name: 'slice', pluralName: 'slices' }, { constructor: tracing.trace_model.InstantEvent, name: 'instantEvent', pluralName: 'instantEvents' }, { constructor: tracing.trace_model.CounterSample, name: 'counterSample', pluralName: 'counterSamples' }, { constructor: tracing.trace_model.ObjectSnapshot, name: 'objectSnapshot', pluralName: 'objectSnapshots' }, { constructor: tracing.trace_model.ObjectInstance, name: 'objectInstance', pluralName: 'objectInstances' }, { constructor: tracing.trace_model.Sample, name: 'sample', pluralName: 'samples' } ]; /** * Represents a selection within a and its associated set of tracks. * @constructor */ function Selection(opt_events) { this.bounds_dirty_ = true; this.bounds_ = new base.Range(); this.length_ = 0; this.guid_ = base.GUID.allocate(); if (opt_events) { for (var i = 0; i < opt_events.length; i++) this.push(opt_events[i]); } } Selection.prototype = { __proto__: Object.prototype, get bounds() { if (this.bounds_dirty_) { this.bounds_.reset(); for (var i = 0; i < this.length_; i++) this[i].addBoundsToRange(this.bounds_); this.bounds_dirty_ = false; } return this.bounds_; }, get duration() { if (this.bounds_.isEmpty) return 0; return this.bounds_.max - this.bounds_.min; }, get length() { return this.length_; }, get guid() { return this.guid_; }, clear: function() { for (var i = 0; i < this.length_; ++i) delete this[i]; this.length_ = 0; this.bounds_dirty_ = true; }, push: function(event) { this[this.length_++] = event; this.bounds_dirty_ = true; return event; }, addSelection: function(selection) { for (var i = 0; i < selection.length; i++) this.push(selection[i]); }, subSelection: function(index, count) { count = count || 1; var selection = new Selection(); selection.bounds_dirty_ = true; if (index < 0 || index + count > this.length_) throw new Error('Index out of bounds'); for (var i = index; i < index + count; i++) selection.push(this[i]); return selection; }, getEventsOrganizedByType: function() { var events = {}; EVENT_TYPES.forEach(function(eventType) { events[eventType.pluralName] = new Selection(); }); for (var i = 0; i < this.length_; i++) { var event = this[i]; EVENT_TYPES.forEach(function(eventType) { if (event instanceof eventType.constructor) events[eventType.pluralName].push(event); }); } return events; }, enumEventsOfType: function(type, func) { for (var i = 0; i < this.length_; i++) if (this[i] instanceof type) func(this[i]); }, map: function(fn) { for (var i = 0; i < this.length_; i++) fn(this[i]); }, /** * Helper for selection previous or next. * @param {boolean} forwardp If true, select one forward (next). * Else, select previous. * * @param {TimelineViewport} viewport The viewport to use to determine what * is near to the current selection. * * @return {boolean} true if current selection changed. */ getShiftedSelection: function(viewport, offset) { var newSelection = new Selection(); for (var i = 0; i < this.length_; i++) { var event = this[i]; var track = viewport.trackForEvent(event); track.addItemNearToProvidedEventToSelection( event, offset, newSelection); } if (newSelection.length == 0) return undefined; return newSelection; } }; return { Selection: Selection }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireStylesheet('tracing.analysis.analysis_link'); base.require('base.events'); base.require('tracing.selection'); base.require('tracing.analysis.util'); base.require('ui'); base.exportTo('tracing.analysis', function() { var tsRound = tracing.analysis.tsRound; var RequestSelectionChangeEvent = base.Event.bind( undefined, 'requestSelectionChange', true, false); /** * A clickable link that requests a change of selection to the return value of * this.selectionGenerator when clicked. * * @constructor */ var AnalysisLink = ui.define('a'); AnalysisLink.prototype = { __proto__: HTMLAnchorElement.prototype, decorate: function() { this.classList.add('analysis-link'); this.selectionGenerator; this.addEventListener('click', this.onClicked_.bind(this)); }, onClicked_: function() { var event = new RequestSelectionChangeEvent(); event.selection = this.selectionGenerator(); this.dispatchEvent(event); } }; /** * Changes the selection to the given ObjectSnapshot when clicked. * @constructor */ var ObjectSnapshotLink = ui.define( 'object-snapshot-link', AnalysisLink); ObjectSnapshotLink.prototype = { __proto__: AnalysisLink.prototype, decorate: function() { AnalysisLink.prototype.decorate.apply(this); }, set objectSnapshot(snapshot) { this.textContent = snapshot.objectInstance.typeName + ' ' + snapshot.objectInstance.id + ' @ ' + tsRound(snapshot.ts) + ' ms'; this.selectionGenerator = function() { var selection = new tracing.Selection(); selection.push(snapshot); return selection; }.bind(this); } }; /** * Changes the selection to the given ObjectInstance when clicked. * @constructor */ var ObjectInstanceLink = ui.define( 'object-instance-link', AnalysisLink); ObjectInstanceLink.prototype = { __proto__: AnalysisLink.prototype, decorate: function() { AnalysisLink.prototype.decorate.apply(this); }, set objectInstance(instance) { this.textContent = instance.typeName + ' ' + instance.id; this.selectionGenerator = function() { var selection = new tracing.Selection(); selection.push(instance); return selection; }.bind(this); } }; return { RequestSelectionChangeEvent: RequestSelectionChangeEvent, AnalysisLink: AnalysisLink, ObjectSnapshotLink: ObjectSnapshotLink, ObjectInstanceLink: ObjectInstanceLink }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireStylesheet('tracing.analysis.generic_object_view'); base.require('base.utils'); base.require('tracing.analysis.analysis_link'); base.require('ui'); base.exportTo('tracing.analysis', function() { /** * @constructor */ var GenericObjectView = ui.define('x-generic-object-view'); GenericObjectView.prototype = { __proto__: HTMLUnknownElement.prototype, decorate: function() { this.object_ = undefined; }, get object() { return this.object_; }, set object(object) { this.object_ = object; this.updateContents_(); }, updateContents_: function() { this.textContent = ''; this.appendElementsForType_('', this.object_, 0, 0, 5, ''); }, appendElementsForType_: function( label, object, indent, depth, maxDepth, suffix) { if (depth > maxDepth) { this.appendSimpleText_( label, indent, '', suffix); return; } if (object === undefined) { this.appendSimpleText_(label, indent, 'undefined', suffix); return; } if (object === null) { this.appendSimpleText_(label, indent, 'null', suffix); return; } if (!(object instanceof Object)) { var type = typeof object; if (type == 'string') { this.appendSimpleText_(label, indent, '"' + object + '"', suffix); } else { this.appendSimpleText_(label, indent, object, suffix); } return; } if (object instanceof tracing.trace_model.ObjectSnapshot) { var link = new tracing.analysis.ObjectSnapshotLink(object); link.objectSnapshot = object; this.appendElementWithLabel_(label, indent, link, suffix); return; } if (object instanceof tracing.trace_model.ObjectInstance) { var link = new tracing.analysis.ObjectInstanceLink(object); link.objectInstance = object; this.appendElementWithLabel_(label, indent, link, suffix); return; } if (object instanceof base.Rect) { this.appendSimpleText_(label, indent, object.toString(), suffix); return; } if (object instanceof Array) { this.appendElementsForArray_( label, object, indent, depth, maxDepth, suffix); return; } this.appendElementsForObject_( label, object, indent, depth, maxDepth, suffix); }, appendElementsForArray_: function( label, object, indent, depth, maxDepth, suffix) { if (object.length == 0) { this.appendSimpleText_(label, indent, '[]', suffix); return; } this.appendElementsForType_( label + '[', object[0], indent, depth + 1, maxDepth, object.length > 1 ? ',' : ']' + suffix); for (var i = 1; i < object.length; i++) { this.appendElementsForType_( '', object[i], indent + label.length + 1, depth + 1, maxDepth, i < object.length - 1 ? ',' : ']' + suffix); } return; }, appendElementsForObject_: function( label, object, indent, depth, maxDepth, suffix) { var keys = base.dictionaryKeys(object); if (keys.length == 0) { this.appendSimpleText_(label, indent, '{}', suffix); return; } this.appendElementsForType_( label + '{' + keys[0] + ': ', object[keys[0]], indent, depth, maxDepth, keys.length > 1 ? ',' : '}' + suffix); for (var i = 1; i < keys.length; i++) { this.appendElementsForType_( keys[i] + ': ', object[keys[i]], indent + label.length + 1, depth + 1, maxDepth, i < keys.length - 1 ? ',' : '}' + suffix); } }, appendElementWithLabel_: function(label, indent, dataElement, suffix) { var row = document.createElement('div'); var indentSpan = document.createElement('span'); indentSpan.style.whiteSpace = 'pre'; for (var i = 0; i < indent; i++) indentSpan.textContent += ' '; row.appendChild(indentSpan); var labelSpan = document.createElement('span'); labelSpan.textContent = label; row.appendChild(labelSpan); row.appendChild(dataElement); var suffixSpan = document.createElement('span'); suffixSpan.textContent = suffix; row.appendChild(suffixSpan); row.dataElement = dataElement; this.appendChild(row); }, appendSimpleText_: function(label, indent, text, suffix) { var el = this.ownerDocument.createElement('span'); el.textContent = text; this.appendElementWithLabel_(label, indent, el, suffix); return el; } }; /** * @constructor */ var GenericObjectViewWithLabel = ui.define( 'x-generic-object-view-with-label'); GenericObjectViewWithLabel.prototype = { __proto__: HTMLUnknownElement.prototype, decorate: function() { this.labelEl_ = document.createElement('div'); this.genericObjectView_ = new tracing.analysis.GenericObjectView(); this.appendChild(this.labelEl_); this.appendChild(this.genericObjectView_); }, get label() { return this.labelEl_.textContent; }, set label(label) { this.labelEl_.textContent = label; }, get object() { return this.genericObjectView_.object; }, set object(object) { this.genericObjectView_.object = object; } }; return { GenericObjectView: GenericObjectView, GenericObjectViewWithLabel: GenericObjectViewWithLabel }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireStylesheet('tracing.analysis.analysis_results'); base.require('tracing.analysis.util'); base.require('tracing.analysis.analysis_link'); base.require('tracing.analysis.generic_object_view'); base.require('ui'); base.exportTo('tracing.analysis', function() { var AnalysisResults = ui.define('div'); AnalysisResults.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { this.className = 'analysis-results'; }, get requiresTallView() { return false; }, clear: function() { this.textContent = ''; }, createSelectionChangingLink: function(text, selectionGenerator, opt_tooltip) { var el = this.ownerDocument.createElement('a'); tracing.analysis.AnalysisLink.decorate(el); el.textContent = text; el.selectionGenerator = selectionGenerator; if (opt_tooltip) el.title = opt_tooltip; return el; }, appendElement_: function(parent, tagName, opt_text) { var n = parent.ownerDocument.createElement(tagName); parent.appendChild(n); if (opt_text != undefined) n.textContent = opt_text; return n; }, appendText_: function(parent, text) { var textElement = parent.ownerDocument.createTextNode(text); parent.appendChild(textNode); return textNode; }, appendTableCell_: function(table, row, cellnum, text) { var td = this.appendElement_(row, 'td', text); td.className = table.className + '-col-' + cellnum; return td; }, appendTableCell: function(table, row, text) { return this.appendTableCell_(table, row, row.children.length, text); }, appendTableCellWithTooltip_: function(table, row, cellnum, text, tooltip) { if (tooltip) { var td = this.appendElement_(row, 'td'); td.className = table.className + '-col-' + cellnum; var span = this.appendElement_(td, 'span', text); span.className = 'tooltip'; span.title = tooltip; return td; } else { this.appendTableCell_(table, row, cellnum, text); } }, /** * Adds a table with the given className. * @return {HTMLTableElement} The newly created table. */ appendTable: function(className, numColumns) { var table = this.appendElement_(this, 'table'); table.headerRow = this.appendElement_(table, 'tr'); table.className = className + ' analysis-table'; table.numColumns = numColumns; return table; }, /** * Creates and appends a row to |table| with a left-aligned |label] * header that spans all columns. */ appendTableHeader: function(table, label) { var th = this.appendElement_(table.headerRow, 'th', label); th.className = 'analysis-table-header'; }, appendTableRow: function(table) { return this.appendElement_(table, 'tr'); }, /** * Creates and appends a row to |table| with a left-aligned |label] * in the first column and an optional |opt_value| in the second * column. */ appendSummaryRow: function(table, label, opt_value) { var row = this.appendElement_(table, 'tr'); row.className = 'analysis-table-row'; this.appendTableCell_(table, row, 0, label); if (opt_value !== undefined) { var objectView = new tracing.analysis.GenericObjectView(); objectView.object = opt_value; objectView.classList.add('analysis-table-col-1'); objectView.style.display = 'table-cell'; row.appendChild(objectView); } else { this.appendTableCell_(table, row, 1, ''); } for (var i = 2; i < table.numColumns; i++) this.appendTableCell_(table, row, i, ''); }, /** * Adds a spacing row to spread out results. */ appendSpacingRow: function(table) { var row = this.appendElement_(table, 'tr'); row.className = 'analysis-table-row'; for (var i = 0; i < table.numColumns; i++) this.appendTableCell_(table, row, i, ' '); }, /** * Creates and appends a row to |table| with a left-aligned |label] * in the first column and a millisecvond |time| value in the second * column. */ appendSummaryRowTime: function(table, label, time) { this.appendSummaryRow(table, label, tracing.analysis.tsRound(time) + ' ms'); }, /** * Creates and appends a row to |table| that summarizes one or more slices, * or one or more counters. * The row has a left-aligned |label| in the first column, the |duration| * of the data in the second, the number of |occurrences| in the third. * @param {object=} opt_statistics May be undefined, or an object which * contains calculated staistics containing min/max/avg for slices, or * min/max/avg/start/end for counters. */ appendDataRow: function( table, label, opt_duration, opt_occurences, opt_statistics, opt_selectionGenerator) { var tooltip = undefined; if (opt_statistics) { tooltip = 'Min Duration:\u0009' + tracing.analysis.tsRound(opt_statistics.min) + ' ms \u000DMax Duration:\u0009' + tracing.analysis.tsRound(opt_statistics.max) + ' ms \u000DAvg Duration:\u0009' + tracing.analysis.tsRound(opt_statistics.avg) + ' ms (\u03C3 = ' + tracing.analysis.tsRound(opt_statistics.avg_stddev) + ')'; if (opt_statistics.start) { tooltip += '\u000DStart Time:\u0009' + tracing.analysis.tsRound(opt_statistics.start) + ' ms'; } if (opt_statistics.end) { tooltip += '\u000DEnd Time:\u0009' + tracing.analysis.tsRound(opt_statistics.end) + ' ms'; } if (opt_statistics.frequency && opt_statistics.frequency_stddev) { tooltip += '\u000DFrequency:\u0009' + tracing.analysis.tsRound(opt_statistics.frequency) + ' occurrences/s (\u03C3 = ' + tracing.analysis.tsRound(opt_statistics.frequency_stddev) + ')'; } } var row = this.appendElement_(table, 'tr'); row.className = 'analysis-table-row'; if (!opt_selectionGenerator) { this.appendTableCellWithTooltip_(table, row, 0, label, tooltip); } else { var labelEl = this.appendTableCellWithTooltip_( table, row, 0, label, tooltip); labelEl.textContent = ''; labelEl.appendChild( this.createSelectionChangingLink(label, opt_selectionGenerator, tooltip)); } if (opt_duration !== undefined) { if (opt_duration instanceof Array) { this.appendTableCellWithTooltip_(table, row, 1, '[' + opt_duration.join(', ') + ']', tooltip); } else { this.appendTableCellWithTooltip_(table, row, 1, tracing.analysis.tsRound(opt_duration) + ' ms', tooltip); } } else { this.appendTableCell_(table, row, 1, ''); } if (opt_occurences !== undefined) { this.appendTableCellWithTooltip_(table, row, 2, String(opt_occurences) + ' occurrences', tooltip); } else { this.appendTableCell_(table, row, 2, ''); } } }; return { AnalysisResults: AnalysisResults }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.analysis.util'); base.require('ui'); base.require('tracing.trace_model.counter_sample'); base.exportTo('tracing.analysis', function() { var CounterSample = tracing.trace_model.CounterSample; function analyzeCounterSamples(results, allSamples) { var samplesByCounter = {}; for (var i = 0; i < allSamples.length; i++) { var ctr = allSamples[i].series.counter; if (!samplesByCounter[ctr.guid]) samplesByCounter[ctr.guid] = []; samplesByCounter[ctr.guid].push(allSamples[i]); } for (var guid in samplesByCounter) { var samples = samplesByCounter[guid]; var ctr = samples[0].series.counter; var timestampGroups = CounterSample.groupByTimestamp(samples); if (timestampGroups.length == 1) analyzeSingleCounterTimestamp(results, ctr, timestampGroups[0]); else analyzeMultipleCounterTimestamps(results, ctr, timestampGroups); } } function analyzeSingleCounterTimestamp( results, ctr, samplesWithSameTimestamp) { var table = results.appendTable('analysis-counter-table', 2); results.appendTableHeader(table, 'Selected counter:'); results.appendSummaryRow(table, 'Title', ctr.name); results.appendSummaryRowTime( table, 'Timestamp', samplesWithSameTimestamp[0].timestamp); for (var i = 0; i < samplesWithSameTimestamp.length; i++) { var sample = samplesWithSameTimestamp[i]; results.appendSummaryRow(table, sample.series.name, sample.value); } } function analyzeMultipleCounterTimestamps(results, ctr, samplesByTimestamp) { var table = results.appendTable('analysis-counter-table', 2); results.appendTableHeader(table, 'Counter ' + ctr.name); var sampleIndices = []; for (var i = 0; i < samplesByTimestamp.length; i++) sampleIndices.push(samplesByTimestamp[i][0].getSampleIndex()); var stats = ctr.getSampleStatistics(sampleIndices); for (var i = 0; i < stats.length; i++) { var samples = []; for (var k = 0; k < sampleIndices.length; ++k) samples.push(ctr.getSeries(i).getSample(sampleIndices[k]).value); results.appendDataRow( table, ctr.name + ': series(' + ctr.getSeries(i).name + ')', samples, samples.length, stats[i]); } } return { analyzeCounterSamples: analyzeCounterSamples, analyzeSingleCounterTimestamp: analyzeSingleCounterTimestamp, analyzeMultipleCounterTimestamps: analyzeMultipleCounterTimestamps }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireStylesheet('tracing.analysis.analyze_slices'); base.require('tracing.analysis.util'); base.require('ui'); base.exportTo('tracing.analysis', function() { function analyzeSingleSlice(results, slice) { var table = results.appendTable('analysis-slice-table', 2); results.appendTableHeader(table, 'Selected slice:'); results.appendSummaryRow(table, 'Title', slice.title); if (slice.category) results.appendSummaryRow(table, 'Category', slice.category); results.appendSummaryRowTime(table, 'Start', slice.start); results.appendSummaryRowTime(table, 'Duration', slice.duration); if (slice.durationInUserTime) { results.appendSummaryRowTime( table, 'Duration (U)', slice.durationInUserTime); } var n = 0; for (var argName in slice.args) { n += 1; } if (n > 0) { results.appendSummaryRow(table, 'Args'); for (var argName in slice.args) { var argVal = slice.args[argName]; // TODO(sleffler) use span instead? results.appendSummaryRow(table, ' ' + argName, argVal); } } } function analyzeMultipleSlices(results, slices) { var tsLo = slices.bounds.min; var tsHi = slices.bounds.max; var numTitles = 0; var slicesByTitle = {}; for (var i = 0; i < slices.length; i++) { var slice = slices[i]; if (slicesByTitle[slice.title] === undefined) { slicesByTitle[slice.title] = []; numTitles++; } var sliceGroup = slicesByTitle[slice.title]; sliceGroup.push(slices[i]); } var table; table = results.appendTable('analysis-slices-table', 3); results.appendTableHeader(table, 'Slices:'); var totalDuration = 0; base.iterItems(slicesByTitle, function(sliceGroupTitle, sliceGroup) { var duration = 0; var avg = 0; var startOfFirstOccurrence = Number.MAX_VALUE; var startOfLastOccurrence = -Number.MAX_VALUE; var frequencyDetails = undefined; var min = Number.MAX_VALUE; var max = -Number.MAX_VALUE; for (var i = 0; i < sliceGroup.length; i++) { var slice = sliceGroup[i]; duration += slice.duration; startOfFirstOccurrence = Math.min(slice.start, startOfFirstOccurrence); startOfLastOccurrence = Math.max(slice.start, startOfLastOccurrence); min = Math.min(slice.duration, min); max = Math.max(slice.duration, max); } totalDuration += duration; if (sliceGroup.length == 0) avg = 0; avg = duration / sliceGroup.length; var statistics = {min: min, max: max, avg: avg, avg_stddev: undefined, frequency: undefined, frequency_stddev: undefined}; // Compute the stddev of the slice durations. var sumOfSquaredDistancesToMean = 0; for (var i = 0; i < sliceGroup.length; i++) { var signedDistance = statistics.avg - sliceGroup[i].duration; sumOfSquaredDistancesToMean += signedDistance * signedDistance; } statistics.avg_stddev = Math.sqrt( sumOfSquaredDistancesToMean / (sliceGroup.length - 1)); // We require at least 3 samples to compute the stddev. var elapsed = startOfLastOccurrence - startOfFirstOccurrence; if (sliceGroup.length > 2 && elapsed > 0) { var numDistances = sliceGroup.length - 1; statistics.frequency = (1000 * numDistances) / elapsed; // Compute the stddev. sumOfSquaredDistancesToMean = 0; for (var i = 1; i < sliceGroup.length; i++) { var currentFrequency = 1000 / (sliceGroup[i].start - sliceGroup[i - 1].start); var signedDistance = statistics.frequency - currentFrequency; sumOfSquaredDistancesToMean += signedDistance * signedDistance; } statistics.frequency_stddev = Math.sqrt( sumOfSquaredDistancesToMean / (numDistances - 1)); } results.appendDataRow( table, sliceGroupTitle, duration, sliceGroup.length, statistics, function() { return new tracing.Selection(sliceGroup); }); // The whole selection is a single type so list out the information // for each sub slice. if (numTitles === 1) { for (var i = 0; i < sliceGroup.length; i++) { analyzeSingleSlice(results, sliceGroup[i]); } } }); // Only one row so we already know the totals. if (numTitles !== 1) { results.appendDataRow(table, '*Totals', totalDuration, slices.length); results.appendSpacingRow(table); } results.appendSummaryRowTime(table, 'Selection start', tsLo); results.appendSummaryRowTime(table, 'Selection extent', tsHi - tsLo); } return { analyzeSingleSlice: analyzeSingleSlice, analyzeMultipleSlices: analyzeMultipleSlices }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.analysis.analyze_counters'); base.require('tracing.analysis.analyze_slices'); base.require('tracing.analysis.util'); base.require('ui'); base.exportTo('tracing.analysis', function() { /** * Analyzes the selection, outputting the analysis results into the provided * results object. * * @param {AnalysisResults} results Where the analysis is placed. * @param {Selection} selection What to analyze. */ function analyzeSelection(results, selection) { analyzeEventsByType(results, selection.getEventsOrganizedByType()); } function analyzeEventsByType(results, eventsByType) { var sliceEvents = eventsByType.slices; var counterSampleEvents = eventsByType.counterSamples; var instantEvents = eventsByType.instantEvents; var sampleEvents = eventsByType.samples; var objectEvents = new tracing.Selection(); objectEvents.addSelection(eventsByType.objectSnapshots); objectEvents.addSelection(eventsByType.objectInstances); if (sliceEvents.length == 1) { tracing.analysis.analyzeSingleSlice(results, sliceEvents[0]); } else if (sliceEvents.length > 1) { tracing.analysis.analyzeMultipleSlices(results, sliceEvents); } if (instantEvents.length == 1) { tracing.analysis.analyzeSingleSlice(results, instantEvents[0]); } else if (instantEvents.length > 1) { tracing.analysis.analyzeMultipleSlices(results, instantEvents); } if (sampleEvents.length == 1) { tracing.analysis.analyzeSingleSlice(results, sampleEvents[0]); } else if (sampleEvents.length > 1) { tracing.analysis.analyzeMultipleSlices(results, sampleEvents); } if (counterSampleEvents.length != 0) tracing.analysis.analyzeCounterSamples(results, counterSampleEvents); if (objectEvents.length) analyzeObjectEvents(results, objectEvents); } /** * Extremely simplistic analysis of objects. Mainly exists to provide * click-through to the main object's analysis view. */ function analyzeObjectEvents(results, objectEvents) { objectEvents = base.asArray(objectEvents).sort( base.Range.compareByMinTimes); var table = results.appendTable('analysis-object-sample-table', 2); results.appendTableHeader(table, 'Selected Objects:'); objectEvents.forEach(function(event) { var row = results.appendTableRow(table); var ts; var objectText; var selectionGenerator; if (event instanceof tracing.trace_model.ObjectSnapshot) { var objectSnapshot = event; ts = tracing.analysis.tsRound(objectSnapshot.ts); objectText = objectSnapshot.objectInstance.typeName + ' ' + objectSnapshot.objectInstance.id; selectionGenerator = function() { var selection = new tracing.Selection(); selection.push(objectSnapshot); return selection; }; } else { var objectInstance = event; var deletionTs = objectInstance.deletionTs == Number.MAX_VALUE ? '' : tracing.analysis.tsRound(objectInstance.deletionTs); ts = tracing.analysis.tsRound(objectInstance.creationTs) + '-' + deletionTs; objectText = objectInstance.typeName + ' ' + objectInstance.id; selectionGenerator = function() { var selection = new tracing.Selection(); selection.push(objectInstance); return selection; }; } results.appendTableCell(table, row, ts); var linkContainer = results.appendTableCell(table, row, ''); linkContainer.appendChild( results.createSelectionChangingLink(objectText, selectionGenerator)); }); } return { analyzeSelection: analyzeSelection, analyzeEventsByType: analyzeEventsByType }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('tracing.analysis.generic_object_view'); base.require('tracing.analysis.analyze_selection'); base.require('tracing.analysis.analysis_results'); base.exportTo('cc', function() { var tsRound = tracing.analysis.tsRound; var GenericObjectViewWithLabel = tracing.analysis.GenericObjectViewWithLabel; function Selection() { this.selectionToSetIfClicked = undefined; }; Selection.prototype = { /** * When two things are picked in the UI, one must occasionally tie-break * between them to decide what was really clicked. Things with higher * specicifity will win. */ get specicifity() { throw new Error('Not implemented'); }, /** * If a selection is related to a specific layer, then this returns the * layerId of that layer. If the selection is not related to a layer, for * example if the device viewport is selected, then this returns undefined. */ get associatedLayerId() { throw new Error('Not implemented'); }, /** * If the selected item(s) is visible on the pending tree in a way that * should be highlighted, returns the quad for the item on the pending tree. * Otherwise, returns undefined. */ get quadIfPending() { throw new Error('Not implemented'); }, /** * If the selected item(s) is visible on the active tree in a way that * should be highlighted, returns the quad for the item on the active tree. * Otherwise, returns undefined. */ get quadIfActive() { throw new Error('Not implemented'); }, /** * A stable string describing what is selected. Used to determine a stable * color of the highlight quads for this selection. */ get title() { throw new Error('Not implemented'); }, /** * Called when the selection is made active in the layer view. Must return * an HTMLElement that explains this selection in detail. */ createAnalysis: function() { throw new Error('Not implemented'); }, /** * Should try to create the equivalent selection in the provided LTHI, * or undefined if it can't be done. */ findEquivalent: function(lthi) { throw new Error('Not implemented'); } }; /** * @constructor */ function LayerSelection(layer) { if (!layer) throw new Error('Layer is required'); this.layer_ = layer; } LayerSelection.prototype = { __proto__: Selection.prototype, get specicifity() { return 1; }, get associatedLayerId() { return this.layer_.layerId; }, get quadIfPending() { return undefined; }, get quadIfActive() { return undefined; }, createAnalysis: function() { var dataView = new GenericObjectViewWithLabel(); dataView.label = 'Layer ' + this.layer_.layerId; dataView.object = this.layer_.args; return dataView; }, get title() { return this.layer_.objectInstance.typeName; }, findEquivalent: function(lthi) { var layer = lthi.activeTree.findLayerWithId(this.layer_.layerId) || lthi.pendingTree.findLayerWithId(this.layer_.layerId); if (!layer) return undefined; return new LayerSelection(layer); } }; /** * @constructor */ function TileSelection(tile) { this.tile_ = tile; } TileSelection.prototype = { __proto__: Selection.prototype, get specicifity() { return 2; }, get associatedLayerId() { return this.tile_.layerId; }, get layerRect() { return this.tile_.layerRect; }, createAnalysis: function() { var analysis = new GenericObjectViewWithLabel(); analysis.label = 'Tile ' + this.tile_.objectInstance.id + ' on layer ' + this.tile_.layerId; analysis.object = this.tile_.args; return analysis; }, get title() { return this.tile_.objectInstance.typeName; }, findEquivalent: function(lthi) { var tileInstance = this.tile_.tileInstance; if (lthi.ts < tileInstance.creationTs || lthi.ts >= tileInstance.deletionTs) return undefined; var tileSnapshot = tileInstance.getSnapshotAt(lthi.ts); if (!tileSnapshot) return undefined; return new TileSelection(tileSnapshot); } }; /** * @constructor */ function LayerRectSelection(layer, rectType, rect, opt_data) { this.layer_ = layer; this.rectType_ = rectType; this.rect_ = rect; this.data_ = opt_data !== undefined ? opt_data : rect; } LayerRectSelection.prototype = { __proto__: Selection.prototype, get specicifity() { return 2; }, get associatedLayerId() { return this.layer_.layerId; }, get layerRect() { return this.rect_; }, createAnalysis: function() { var analysis = new GenericObjectViewWithLabel(); analysis.label = this.rectType_ + ' on layer ' + this.layer_.layerId; analysis.object = this.data_; return analysis; }, get title() { return this.rectType_; }, findEquivalent: function(lthi) { return undefined; } }; /** * @constructor */ function RasterTaskSelection(rasterTask) { this.rasterTask_ = rasterTask; } RasterTaskSelection.prototype = { __proto__: Selection.prototype, get specicifity() { return 3; }, get tile() { return this.rasterTask_.args.data.tile_id; }, get associatedLayerId() { return this.tile.layerId; }, get layerRect() { return this.tile.layerRect; }, createAnalysis: function() { var sel = new tracing.Selection(); sel.push(this.rasterTask_); var analysis = new tracing.analysis.AnalysisResults(); tracing.analysis.analyzeSelection(analysis, sel); return analysis; }, get title() { return this.rasterTask_.title; }, findEquivalent: function(lthi) { // Raster tasks are only valid in one LTHI. return undefined; } }; return { Selection: Selection, LayerSelection: LayerSelection, TileSelection: TileSelection, LayerRectSelection: LayerRectSelection, RasterTaskSelection: RasterTaskSelection }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('ui'); base.require('base.settings'); base.exportTo('ui', function() { function createSpan(opt_dictionary) { var spanEl = document.createElement('span'); if (opt_dictionary) { if (opt_dictionary.className) spanEl.className = opt_dictionary.className; if (opt_dictionary.textContent) spanEl.textContent = opt_dictionary.textContent; if (opt_dictionary.parent) opt_dictionary.parent.appendChild(spanEl); } return spanEl; }; function createDiv(opt_dictionary) { var divEl = document.createElement('div'); if (opt_dictionary) { if (opt_dictionary.className) divEl.className = opt_dictionary.className; if (opt_dictionary.parent) opt_dictionary.parent.appendChild(divEl); } return divEl; }; function createScopedStyle(styleContent) { var styleEl = document.createElement('style'); styleEl.scoped = true; styleEl.innerHTML = styleContent; return styleEl; } function createSelector( targetEl, targetElProperty, settingsKey, defaultValue, items) { var defaultValueIndex; for (var i = 0; i < items.length; i++) { var item = items[i]; if (item.value == defaultValue) { defaultValueIndex = i; break; } } if (defaultValueIndex === undefined) throw new Error('defaultValue must be in the items list'); var selectorEl = document.createElement('select'); selectorEl.addEventListener('change', onChange); for (var i = 0; i < items.length; i++) { var item = items[i]; var optionEl = document.createElement('option'); optionEl.textContent = item.label; optionEl.targetPropertyValue = item.value; selectorEl.appendChild(optionEl); } function onChange(e) { var value = selectorEl.selectedOptions[0].targetPropertyValue; base.Settings.set(settingsKey, value); targetEl[targetElProperty] = value; } var oldSetter = targetEl.__lookupSetter__('selectedIndex'); selectorEl.__defineGetter__('selectedValue', function(v) { return selectorEl.children[selectorEl.selectedIndex].targetPropertyValue; }); selectorEl.__defineSetter__('selectedValue', function(v) { for (var i = 0; i < selectorEl.children.length; i++) { var value = selectorEl.children[i].targetPropertyValue; if (value == v) { selectorEl.selectedIndex = i; onChange(); return; } } throw new Error('Not a valid value'); }); var initialValue = base.Settings.get(settingsKey, defaultValue); var didSet = false; for (var i = 0; i < selectorEl.children.length; i++) { if (selectorEl.children[i].targetPropertyValue == initialValue) { didSet = true; targetEl[targetElProperty] = initialValue; selectorEl.selectedIndex = i; break; } } if (!didSet) { selectorEl.selectedIndex = defaultValueIndex; targetEl[targetElProperty] = defaultValue; } return selectorEl; } var nextCheckboxId = 1; function createCheckBox(targetEl, targetElProperty, settingsKey, defaultValue, label) { var buttonEl = document.createElement('input'); buttonEl.type = 'checkbox'; var initialValue = base.Settings.get(settingsKey, defaultValue); buttonEl.checked = !!initialValue; if (targetEl) targetEl[targetElProperty] = initialValue; function onChange() { base.Settings.set(settingsKey, buttonEl.checked); if (targetEl) targetEl[targetElProperty] = buttonEl.checked; } buttonEl.addEventListener('change', onChange); var id = '#checkbox-' + nextCheckboxId++; var spanEl = createSpan({className: 'labeled-checkbox'}); buttonEl.setAttribute('id', id); var labelEl = document.createElement('label'); labelEl.textContent = label; labelEl.setAttribute('for', id); spanEl.appendChild(buttonEl); spanEl.appendChild(labelEl); spanEl.__defineSetter__('checked', function(opt_bool) { buttonEl.checked = !!opt_bool; onChange(); }); spanEl.__defineGetter__('checked', function() { return buttonEl.checked; }); return spanEl; } return { createSpan: createSpan, createDiv: createDiv, createScopedStyle: createScopedStyle, createSelector: createSelector, createCheckBox: createCheckBox }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('ui'); base.requireStylesheet('ui.drag_handle'); base.exportTo('ui', function() { /** * Detects when user clicks handle determines new height of container based * on user's vertical mouse move and resizes the target. * @constructor * @extends {HTMLDivElement} * You will need to set target to be the draggable element */ var DragHandle = ui.define('x-drag-handle'); DragHandle.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { this.lastMousePos_ = 0; this.onMouseMove_ = this.onMouseMove_.bind(this); this.onMouseUp_ = this.onMouseUp_.bind(this); this.addEventListener('mousedown', this.onMouseDown_); this.target_ = undefined; this.horizontal = true; this.observer_ = new WebKitMutationObserver( this.didTargetMutate_.bind(this)); this.targetSizesByModeKey_ = {}; }, get modeKey_() { return this.target_.className == '' ? '.' : this.target_.className; }, get target() { return this.target_; }, set target(target) { this.observer_.disconnect(); this.target_ = target; if (!this.target_) return; this.observer_.observe(this.target_, { attributes: true, attributeFilter: ['class'] }); }, get horizontal() { return this.horizontal_; }, set horizontal(h) { this.horizontal_ = h; if (this.horizontal_) this.className = 'horizontal-drag-handle'; else this.className = 'vertical-drag-handle'; }, get vertical() { return !this.horizontal_; }, set vertical(v) { this.horizontal = !v; }, forceMutationObserverFlush_: function() { var records = this.observer_.takeRecords(); if (records.length) this.didTargetMutate_(records); }, didTargetMutate_: function(e) { var modeSize = this.targetSizesByModeKey_[this.modeKey_]; if (modeSize !== undefined) { this.setTargetSize_(modeSize); return; } // If we hadn't previously sized the target, then just remove any manual // sizing that we applied. this.target_.style[this.targetStyleKey_] = ''; }, get targetStyleKey_() { return this.horizontal_ ? 'height' : 'width'; }, getTargetSize_: function() { // If style is not set, start off with computed height. var targetStyleKey = this.targetStyleKey_; if (!this.target_.style[targetStyleKey]) { this.target_.style[targetStyleKey] = window.getComputedStyle(this.target_)[targetStyleKey]; } var size = parseInt(this.target_.style[targetStyleKey]); this.targetSizesByModeKey_[this.modeKey_] = size; return size; }, setTargetSize_: function(s) { this.target_.style[this.targetStyleKey_] = s + 'px'; this.targetSizesByModeKey_[this.modeKey_] = s; }, applyDelta_: function(delta) { // Apply new size to the container. var curSize = this.getTargetSize_(); var newSize; if (this.target_ === this.nextSibling) { newSize = curSize + delta; } else { newSize = curSize - delta; } this.setTargetSize_(newSize); }, onMouseMove_: function(e) { // Compute the difference in height position. var curMousePos = this.horizontal_ ? e.clientY : e.clientX; var delta = this.lastMousePos_ - curMousePos; this.applyDelta_(delta); this.lastMousePos_ = curMousePos; e.preventDefault(); return true; }, onMouseDown_: function(e) { if (!this.target_) return; this.forceMutationObserverFlush_(); this.lastMousePos_ = this.horizontal_ ? e.clientY : e.clientX; document.addEventListener('mousemove', this.onMouseMove_); document.addEventListener('mouseup', this.onMouseUp_); e.preventDefault(); return true; }, onMouseUp_: function(e) { document.removeEventListener('mousemove', this.onMouseMove_); document.removeEventListener('mouseup', this.onMouseUp_); e.preventDefault(); } }; return { DragHandle: DragHandle }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Container that decorates its children. */ base.require('base.events'); base.require('ui'); base.exportTo('ui', function() { /** * @constructor */ var ContainerThatDecoratesItsChildren = ui.define('div'); ContainerThatDecoratesItsChildren.prototype = { __proto__: HTMLUnknownElement.prototype, decorate: function() { this.observer_ = new WebKitMutationObserver(this.didMutate_.bind(this)); this.observer_.observe(this, { childList: true }); // textContent is a variable on regular HTMLElements. However, we want to // hook and prevent writes to it. Object.defineProperty( this, 'textContent', { get: undefined, set: this.onSetTextContent_}); }, appendChild: function(x) { HTMLUnknownElement.prototype.appendChild.call(this, x); this.didMutate_(this.observer_.takeRecords()); }, insertBefore: function(x, y) { HTMLUnknownElement.prototype.insertBefore.call(this, x, y); this.didMutate_(this.observer_.takeRecords()); }, removeChild: function(x) { HTMLUnknownElement.prototype.removeChild.call(this, x); this.didMutate_(this.observer_.takeRecords()); }, replaceChild: function(x, y) { HTMLUnknownElement.prototype.replaceChild.call(this, x, y); this.didMutate_(this.observer_.takeRecords()); }, onSetTextContent_: function(textContent) { if (textContent != '') throw new Error('textContent can only be set to \'\'.'); this.clear(); }, clear: function() { while (this.lastChild) HTMLUnknownElement.prototype.removeChild.call(this, this.lastChild); this.didMutate_(this.observer_.takeRecords()); }, didMutate_: function(records) { this.beginDecorating_(); for (var i = 0; i < records.length; i++) { var addedNodes = records[i].addedNodes; if (addedNodes) { for (var j = 0; j < addedNodes.length; j++) this.decorateChild_(addedNodes[j]); } var removedNodes = records[i].removedNodes; if (removedNodes) { for (var j = 0; j < removedNodes.length; j++) { this.undecorateChild_(removedNodes[j]); } } } this.doneDecoratingForNow_(); }, decorateChild_: function(child) { throw new Error('Not implemented'); }, undecorateChild_: function(child) { throw new Error('Not implemented'); }, beginDecorating_: function() { }, doneDecoratingForNow_: function() { } }; return { ContainerThatDecoratesItsChildren: ContainerThatDecoratesItsChildren }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Simple list view. */ base.requireStylesheet('ui.list_view'); base.require('base.events'); base.require('base.utils'); base.require('ui'); base.require('ui.container_that_decorates_its_children'); base.exportTo('ui', function() { /** * @constructor */ var ListView = ui.define('x-list-view', ui.ContainerThatDecoratesItsChildren); ListView.prototype = { __proto__: ui.ContainerThatDecoratesItsChildren.prototype, decorate: function() { ui.ContainerThatDecoratesItsChildren.prototype.decorate.call(this); this.classList.add('x-list-view'); this.onItemClicked_ = this.onItemClicked_.bind(this); this.onKeyDown_ = this.onKeyDown_.bind(this); this.tabIndex = 0; this.addEventListener('keydown', this.onKeyDown_); this.selectionChanged_ = false; }, decorateChild_: function(item) { item.classList.add('list-item'); item.addEventListener('click', this.onItemClicked_, true); var listView = this; Object.defineProperty( item, 'selected', { configurable: true, set: function(value) { var oldSelection = listView.selectedElement; if (oldSelection && oldSelection != this && value) listView.selectedElement.removeAttribute('selected'); if (value) this.setAttribute('selected', 'selected'); else this.removeAttribute('selected'); var newSelection = listView.selectedElement; if (newSelection != oldSelection) base.dispatchSimpleEvent(listView, 'selection-changed', false); }, get: function() { return this.hasAttribute('selected'); } }); }, undecorateChild_: function(item) { this.selectionChanged_ |= item.selected; item.classList.remove('list-item'); item.removeEventListener('click', this.onItemClicked_); delete item.selected; }, beginDecorating_: function() { this.selectionChanged_ = false; }, doneDecoratingForNow_: function() { if (this.selectionChanged_) base.dispatchSimpleEvent(this, 'selection-changed', false); }, get selectedElement() { var el = this.querySelector('.list-item[selected]'); if (!el) return undefined; return el; }, set selectedElement(el) { if (!el) { if (this.selectedElement) this.selectedElement.selected = false; return; } if (el.parentElement != this) throw new Error( 'Can only select elements that are children of this list view'); el.selected = true; }, getElementByIndex: function(index) { return this.querySelector('.list-item:nth-child(' + index + ')'); }, clear: function() { var changed = this.selectedElement !== undefined; ui.ContainerThatDecoratesItsChildren.prototype.clear.call(this); if (changed) base.dispatchSimpleEvent(this, 'selection-changed', false); }, onItemClicked_: function(e) { var currentSelectedElement = this.selectedElement; if (currentSelectedElement) currentSelectedElement.removeAttribute('selected'); var element = e.target; while (element.parentElement != this) element = element.parentElement; element.setAttribute('selected', 'selected'); base.dispatchSimpleEvent(this, 'selection-changed', false); }, onKeyDown_: function(e) { if (this.selectedElement === undefined) return; if (e.keyCode == 38) { // Up arrow. var prev = this.selectedElement.previousSibling; if (prev) { prev.selected = true; base.scrollIntoViewIfNeeded(prev); e.preventDefault(); return true; } } else if (e.keyCode == 40) { // Down arrow. var next = this.selectedElement.nextSibling; if (next) { next.selected = true; base.scrollIntoViewIfNeeded(next); e.preventDefault(); return true; } } }, addItem: function(textContent) { var item = document.createElement('div'); item.textContent = textContent; this.appendChild(item); return item; } }; return { ListView: ListView }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireStylesheet('cc.layer_picker'); base.require('cc.constants'); base.require('cc.layer_tree_host_impl'); base.require('cc.selection'); base.require('tracing.analysis.generic_object_view'); base.require('tracing.trace_model.event'); base.require('ui.drag_handle'); base.require('ui.list_view'); base.require('ui.dom_helpers'); base.exportTo('cc', function() { var constants = cc.constants; /** * @constructor */ var LayerPicker = ui.define('layer-picker'); LayerPicker.prototype = { __proto__: HTMLUnknownElement.prototype, decorate: function() { this.lthi_ = undefined; this.controls_ = document.createElement('top-controls'); this.layerList_ = new ui.ListView(); this.appendChild(this.controls_); this.appendChild(this.layerList_); this.layerList_.addEventListener( 'selection-changed', this.onLayerSelectionChanged_.bind(this)); this.controls_.appendChild(ui.createSelector( this, 'whichTree', 'layerPicker.whichTree', constants.ACTIVE_TREE, [{label: 'Active tree', value: constants.ACTIVE_TREE}, {label: 'Pending tree', value: constants.PENDING_TREE}])); this.showPureTransformLayers_ = false; var showPureTransformLayers = ui.createCheckBox( this, 'showPureTransformLayers', 'layerPicker.showPureTransformLayers', false, 'Transform layers'); showPureTransformLayers.classList.add('show-transform-layers'); showPureTransformLayers.title = 'When checked, pure transform layers are shown'; this.controls_.appendChild(showPureTransformLayers); }, get lthiSnapshot() { return this.lthiSnapshot_; }, set lthiSnapshot(lthiSnapshot) { this.lthiSnapshot_ = lthiSnapshot; this.updateContents_(); }, get whichTree() { return this.whichTree_; }, set whichTree(whichTree) { this.whichTree_ = whichTree; this.updateContents_(); }, get showPureTransformLayers() { return this.showPureTransformLayers_; }, set showPureTransformLayers(show) { if (this.showPureTransformLayers_ === show) return; this.showPureTransformLayers_ = show; this.updateContents_(); }, getLayerInfos_: function() { if (!this.lthiSnapshot_) return []; var tree = this.lthiSnapshot_.getTree(this.whichTree_); if (!tree) return []; var layerInfos = []; var showPureTransformLayers = this.showPureTransformLayers_; function isPureTransformLayer(layer) { if (layer.args.compositingReasons && layer.args.compositingReasons.length != 1 && layer.args.compositingReasons[0] != 'No reasons given') return false; if (layer.args.drawsContent) return false; return true; } var visitedLayers = {}; function visitLayer(layer, depth, isMask, isReplica) { if (visitedLayers[layer.layerId]) return; visitedLayers[layer.layerId] = true; var info = {layer: layer, depth: depth}; if (layer.args.drawsContent) info.name = layer.objectInstance.name; else info.name = 'cc::LayerImpl'; info.isMaskLayer = isMask; info.replicaLayer = isReplica; if (showPureTransformLayers || !isPureTransformLayer(layer)) layerInfos.push(info); }; tree.iterLayers(visitLayer); return layerInfos; }, updateContents_: function() { this.layerList_.clear(); var selectedLayerId; if (this.selection_ && this.selection_.associatedLayerId) selectedLayerId = this.selection_.associatedLayerId; var layerInfos = this.getLayerInfos_(); layerInfos.forEach(function(layerInfo) { var layer = layerInfo.layer; var item = document.createElement('div'); var indentEl = item.appendChild(ui.createSpan()); indentEl.style.whiteSpace = 'pre'; for (var i = 0; i < layerInfo.depth; i++) indentEl.textContent = indentEl.textContent + ' '; var labelEl = item.appendChild(ui.createSpan()); var id = layer.layerId; labelEl.textContent = layerInfo.name + ' ' + id; var notesEl = item.appendChild(ui.createSpan()); if (layerInfo.isMaskLayer) notesEl.textContent += '(mask)'; if (layerInfo.isReplicaLayer) notesEl.textContent += '(replica)'; item.layer = layer; this.layerList_.appendChild(item); if (layer.layerId == selectedLayerId) layer.selectionState = tracing.trace_model.SelectionState.SELECTED; }, this); }, onLayerSelectionChanged_: function(e) { var selectedLayer; if (this.layerList_.selectedElement) selectedLayer = this.layerList_.selectedElement.layer; if (selectedLayer) this.selection_ = new cc.LayerSelection(selectedLayer); else this.selection_ = undefined; base.dispatchSimpleEvent(this, 'selection-changed', false); }, get selection() { return this.selection_; }, set selection(selection) { this.selection_ = selection; this.updateContents_(); } }; return { LayerPicker: LayerPicker }; }); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.exportTo('base', function() { function Color(opt_r, opt_g, opt_b, opt_a) { this.r = Math.floor(opt_r) || 0; this.g = Math.floor(opt_g) || 0; this.b = Math.floor(opt_b) || 0; this.a = opt_a; } Color.fromString = function(str) { var tmp; var values; if (str.substr(0, 4) == 'rgb(') { tmp = str.substr(4, str.length - 5); values = tmp.split(',').map(function(v) { return v.replace(/^\s+/, '', 'g'); }); if (values.length != 3) throw new Error('Malformatted rgb-expression'); return new Color( parseInt(values[0]), parseInt(values[1]), parseInt(values[2])); } else if (str.substr(0, 5) == 'rgba(') { tmp = str.substr(5, str.length - 6); values = tmp.split(',').map(function(v) { return v.replace(/^\s+/, '', 'g'); }); if (values.length != 4) throw new Error('Malformatted rgb-expression'); return new Color( parseInt(values[0]), parseInt(values[1]), parseInt(values[2]), parseFloat(values[3])); } else if (str[0] == '#' && str.length == 7) { return new Color( parseInt(str.substr(1, 2), 16), parseInt(str.substr(3, 2), 16), parseInt(str.substr(5, 2), 16)); } else { throw new Error('Unrecognized string format.'); } }; Color.lerp = function(a, b, percent) { if (a.a !== undefined && b.a !== undefined) return Color.lerpRGBA(a, b, percent); return Color.lerpRGB(a, b, percent); } Color.lerpRGB = function(a, b, percent) { return new Color( ((b.r - a.r) * percent) + a.r, ((b.g - a.g) * percent) + a.g, ((b.b - a.b) * percent) + a.b); } Color.lerpRGBA = function(a, b, percent) { return new Color( ((b.r - a.r) * percent) + a.r, ((b.g - a.g) * percent) + a.g, ((b.b - a.b) * percent) + a.b, ((b.a - a.a) * percent) + a.a); } Color.prototype = { clone: function() { var c = new Color(); c.r = this.r; c.g = this.g; c.b = this.b; c.a = this.a; return c; }, brighten: function(opt_k) { var k; k = opt_k || 0.45; return new Color( Math.min(255, this.r + Math.floor(this.r * k)), Math.min(255, this.g + Math.floor(this.g * k)), Math.min(255, this.b + Math.floor(this.b * k))); }, darken: function(opt_k) { var k; k = opt_k || 0.45; return new Color( Math.min(255, this.r - Math.floor(this.r * k)), Math.min(255, this.g - Math.floor(this.g * k)), Math.min(255, this.b - Math.floor(this.b * k))); }, withAlpha: function(a) { return new Color(this.r, this.g, this.b, a); }, toString: function() { if (this.a !== undefined) { return 'rgba(' + this.r + ',' + this.g + ',' + this.b + ',' + this.a + ')'; } return 'rgb(' + this.r + ',' + this.g + ',' + this.b + ')'; } }; return { Color: Color }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Mapping of different tile configuration * to border colors and widths. */ base.exportTo('cc', function() { var tileTypes = { highRes: 'highRes', lowRes: 'lowRes', extraHighRes: 'extraHighRes', extraLowRes: 'extraLowRes', missing: 'missing', culled: 'culled', solidColor: 'solidColor', picture: 'picture', directPicture: 'directPicture', unknown: 'unknown' }; var tileBorder = { highRes: { color: 'rgba(80, 200, 200, 0.7)', width: 1 }, lowRes: { color: 'rgba(212, 83, 192, 0.7)', width: 2 }, extraHighRes: { color: 'rgba(239, 231, 20, 0.7)', width: 2 }, extraLowRes: { color: 'rgba(93, 186, 18, 0.7)', width: 2 }, missing: { color: 'rgba(255, 0, 0, 0.7)', width: 1 }, culled: { color: 'rgba(160, 100, 0, 0.8)', width: 1 }, solidColor: { color: 'rgba(128, 128, 128, 0.7)', width: 1 }, picture: { color: 'rgba(64, 64, 64, 0.7)', width: 1 }, directPicture: { color: 'rgba(127, 255, 0, 1.0)', width: 1 }, unknown: { color: 'rgba(0, 0, 0, 1.0)', width: 2 } }; return { tileTypes: tileTypes, tileBorder: tileBorder }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.exportTo('cc', function() { /** * @constructor */ function PictureAsImageData(picture, errorOrImageData) { this.picture_ = picture; if (errorOrImageData instanceof ImageData) { this.error_ = undefined; this.imageData_ = errorOrImageData; } else { this.error_ = errorOrImageData; this.imageData_ = undefined; } }; /** * Creates a new pending PictureAsImageData (no image data and no error). * * @return {PictureAsImageData} a new pending PictureAsImageData. */ PictureAsImageData.Pending = function(picture) { return new PictureAsImageData(picture, undefined); }; PictureAsImageData.prototype = { get picture() { return this.picture_; }, get error() { return this.error_; }, get imageData() { return this.imageData_; }, isPending: function() { return this.error_ === undefined && this.imageData_ === undefined; }, asCanvas: function() { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); if (!this.imageData_) return; canvas.width = this.imageData_.width; canvas.height = this.imageData_.height; ctx.putImageData(this.imageData_, 0, 0); return canvas; } }; return { PictureAsImageData: PictureAsImageData }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.guid'); base.require('base.rect'); base.require('base.raf'); base.require('tracing.trace_model.object_instance'); base.require('cc.picture_as_image_data'); base.require('cc.util'); base.exportTo('cc', function() { var ObjectSnapshot = tracing.trace_model.ObjectSnapshot; // Number of pictures created. Used as an uniqueId because we are immutable. var PictureCount = 0; var OPS_TIMING_ITERATIONS = 3; function Picture(skp64, layerRect, opaqueRect) { this.skp64_ = skp64; this.layerRect_ = layerRect; this.opaqueRect_ = opaqueRect; this.guid_ = base.GUID.allocate(); } Picture.prototype = { get layerRect() { return this.layerRect_; }, get guid() { return this.guid_; }, getBase64SkpData: function() { return this.skp64_; }, getOps: function() { if (!PictureSnapshot.CanGetOps()) { console.error(PictureSnapshot.HowToEnablePictureDebugging()); return undefined; } var ops = window.chrome.skiaBenchmarking.getOps({ skp64: this.skp64_, params: { layer_rect: this.layerRect_.toArray(), opaque_rect: this.opaqueRect_.toArray() } }); if (!ops) console.error('Failed to get picture ops.'); return ops; }, getOpTimings: function() { if (!PictureSnapshot.CanGetOpTimings()) { console.error(PictureSnapshot.HowToEnablePictureDebugging()); return undefined; } var opTimings = window.chrome.skiaBenchmarking.getOpTimings({ skp64: this.skp64_, params: { layer_rect: this.layerRect_.toArray(), opaque_rect: this.opaqueRect_.toArray() } }); if (!opTimings) console.error('Failed to get picture op timings.'); return opTimings; }, /** * Tag each op with the time it takes to rasterize. * * FIXME: We should use real statistics to get better numbers here, see * https://code.google.com/p/trace-viewer/issues/detail?id=357 * * @param {Array} ops Array of Skia operations. * @return {Array} Skia ops where op.cmd_time contains the associated time * for a given op. */ tagOpsWithTimings: function(ops) { var opTimings = new Array(); for (var iteration = 0; iteration < OPS_TIMING_ITERATIONS; iteration++) { opTimings[iteration] = this.getOpTimings(); if (!opTimings[iteration] || !opTimings[iteration].cmd_times) return ops; if (opTimings[iteration].cmd_times.length != ops.length) return ops; } for (var opIndex = 0; opIndex < ops.length; opIndex++) { var min = Number.MAX_VALUE; for (var i = 0; i < OPS_TIMING_ITERATIONS; i++) min = Math.min(min, opTimings[i].cmd_times[opIndex]); ops[opIndex].cmd_time = min; } return ops; }, /** * Rasterize the picture. * * @param {{opt_stopIndex: number, params}} The SkPicture operation to * rasterize up to. If not defined, the entire SkPicture is rasterized. * @param {{opt_showOverdraw: bool, params}} Defines whether pixel overdraw should be visualized in the image. * @param {function(cc.PictureAsImageData)} The callback function that is * called after rasterization is complete or fails. */ rasterize: function(params, rasterCompleteCallback) { if (!PictureSnapshot.CanRasterize() || !PictureSnapshot.CanGetOps()) { rasterCompleteCallback(new cc.PictureAsImageData( this, cc.PictureSnapshot.HowToEnablePictureDebugging())); return; } var raster = window.chrome.skiaBenchmarking.rasterize( { skp64: this.skp64_, params: { layer_rect: this.layerRect_.toArray(), opaque_rect: this.opaqueRect_.toArray() } }, { stop: params.stopIndex === undefined ? -1 : params.stopIndex, overdraw: !!params.showOverdraw, params: { } }); if (raster) { var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); canvas.width = raster.width; canvas.height = raster.height; var imageData = ctx.createImageData(raster.width, raster.height); imageData.data.set(new Uint8ClampedArray(raster.data)); rasterCompleteCallback(new cc.PictureAsImageData(this, imageData)); } else { var error = 'Failed to rasterize picture. ' + 'Your recording may be from an old Chrome version. ' + 'The SkPicture format is not backward compatible.'; rasterCompleteCallback(new cc.PictureAsImageData(this, error)); } } }; /** * @constructor */ function PictureSnapshot() { ObjectSnapshot.apply(this, arguments); } PictureSnapshot.HasSkiaBenchmarking = function() { if (!window.chrome) return false; if (!window.chrome.skiaBenchmarking) return false; return true; } PictureSnapshot.CanRasterize = function() { if (!PictureSnapshot.HasSkiaBenchmarking()) return false; if (!window.chrome.skiaBenchmarking.rasterize) return false; return true; } PictureSnapshot.CanGetOps = function() { if (!PictureSnapshot.HasSkiaBenchmarking()) return false; if (!window.chrome.skiaBenchmarking.getOps) return false; return true; } PictureSnapshot.CanGetOpTimings = function() { if (!PictureSnapshot.HasSkiaBenchmarking()) return false; if (!window.chrome.skiaBenchmarking.getOpTimings) return false; return true; } PictureSnapshot.CanGetInfo = function() { if (!PictureSnapshot.HasSkiaBenchmarking()) return false; if (!window.chrome.skiaBenchmarking.getInfo) return false; return true; } PictureSnapshot.HowToEnablePictureDebugging = function() { var usualReason = [ 'For pictures to show up, you need to have Chrome running with ', '--enable-skia-benchmarking. Please restart chrome with this flag ', 'and try again.' ].join(''); if (!window.chrome) return usualReason; if (!window.chrome.skiaBenchmarking) return usualReason; if (!window.chrome.skiaBenchmarking.rasterize) return 'Your chrome is old'; if (!window.chrome.skiaBenchmarking.getOps) return 'Your chrome is old: skiaBenchmarking.getOps not found'; if (!window.chrome.skiaBenchmarking.getOpTimings) return 'Your chrome is old: skiaBenchmarking.getOpTimings not found'; if (!window.chrome.skiaBenchmarking.getInfo) return 'Your chrome is old: skiaBenchmarking.getInfo not found'; return 'Rasterizing is on'; } PictureSnapshot.prototype = { __proto__: ObjectSnapshot.prototype, preInitialize: function() { cc.preInitializeObject(this); this.rasterResult_ = undefined; }, initialize: function() { // If we have an alias args, that means this picture was represented // by an alias, and the real args is in alias.args. if (this.args.alias) this.args = this.args.alias.args; if (!this.args.params.layerRect) throw new Error('Missing layer rect'); this.layerRect_ = this.args.params.layerRect; this.picture_ = new Picture(this.args.skp64, this.args.params.layerRect, this.args.params.opaqueRect); }, get layerRect() { return this.layerRect_; }, get guid() { return this.picture_.guid; }, getBase64SkpData: function() { return this.picture_.getBase64SkpData(); }, getOps: function() { return this.picture_.getOps(); }, getOpTimings: function() { return this.picture_.getOpTimings(); }, tagOpsWithTimings: function(ops) { return this.picture_.tagOpsWithTimings(ops); }, rasterize: function(params, rasterCompleteCallback) { this.picture_.rasterize(params, rasterCompleteCallback); } }; ObjectSnapshot.register('cc::Picture', PictureSnapshot); return { PictureSnapshot: PictureSnapshot, Picture: Picture }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.rect'); base.require('tracing.trace_model.object_instance'); base.require('cc.util'); base.require('cc.debug_colors'); base.exportTo('cc', function() { var ObjectSnapshot = tracing.trace_model.ObjectSnapshot; /** * @constructor */ function TileSnapshot() { ObjectSnapshot.apply(this, arguments); } TileSnapshot.prototype = { __proto__: ObjectSnapshot.prototype, preInitialize: function() { cc.preInitializeObject(this); }, initialize: function() { cc.moveOptionalFieldsFromArgsToToplevel( this, ['layerId', 'contentsScale', 'contentRect']); if (this.args.managedState) { this.resolution = this.args.managedState.resolution; this.isSolidColor = this.args.managedState.isSolidColor; this.hasResource = this.args.managedState.hasResource; this.scheduledPriority = this.args.managedState.scheduledPriority; this.distanceToVisible = this.args.managedState.distanceToVisibleInPixels; this.timeToVisible = this.args.managedState.timeToNeededInSeconds; } else { this.resolution = 'HIGH_RESOLUTION'; this.isSolidColor = false; this.hasResource = false; this.scheduledPriority = undefined; this.distanceToVisible = undefined; this.timeToVisible = undefined; } if (this.timeToVisible > 60) this.timeToVisible = 60; // This check is for backward compatability. It can probably // be removed once we're confident that most traces contain // content_rect. if (this.contentRect) this.layerRect = this.contentRect.scale(1.0 / this.contentsScale); if (this.isSolidColor) this.type_ = cc.tileTypes.solidColor; else if (!this.hasResource) this.type_ = cc.tileTypes.missing; else if (this.resolution === 'HIGH_RESOLUTION') this.type_ = cc.tileTypes.highRes; else if (this.resolution === 'LOW_RESOLUTION') this.type_ = cc.tileTypes.lowRes; else this.type_ = cc.tileTypes.unknown; }, getTypeForLayer: function(layer) { var type = this.type_; if (type == cc.tileTypes.unknown) { if (this.contentsScale < layer.idealContentsScale) type = cc.tileTypes.extraLowRes; else if (this.contentsScale > layer.idealContentsScale) type = cc.tileTypes.extraHighRes; } return type; } }; ObjectSnapshot.register('cc::Tile', TileSnapshot); return { TileSnapshot: TileSnapshot }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireStylesheet('ui.info_bar'); base.require('ui'); base.require('ui.dom_helpers'); base.exportTo('ui', function() { /** * @constructor */ var InfoBar = ui.define('info-bar'); InfoBar.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { this.messageEl_ = ui.createSpan({className: 'message'}); this.buttonsEl_ = ui.createSpan({className: 'buttons'}); this.appendChild(this.messageEl_); this.appendChild(this.buttonsEl_); this.message = ''; this.visible = false; }, get message() { return this.messageEl_.textContent; }, set message(message) { this.messageEl_.textContent = message; }, get visible() { return this.classList.contains('info-bar-hidden'); }, set visible(visible) { if (visible) this.classList.remove('info-bar-hidden'); else this.classList.add('info-bar-hidden'); }, removeAllButtons: function() { this.buttonsEl_.textContent = ''; }, addButton: function(text, clickCallback) { var button = document.createElement('button'); button.textContent = text; button.addEventListener('click', clickCallback); this.buttonsEl_.appendChild(button); return button; } }; return { InfoBar: InfoBar }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('ui'); base.exportTo('ui', function() { var constants = { DEFAULT_SCALE: 0.5, MINIMUM_SCALE: 0.1, MAXIMUM_SCALE: 2.0, RESCALE_TIMEOUT_MS: 200, MAXIMUM_TILT: 90 // degrees }; var Camera = ui.define('camera'); Camera.prototype = { __proto__: HTMLUnknownElement.prototype, decorate: function(eventSource) { this.eventSource_ = eventSource; this.eventSource_.addEventListener('beginpan', this.onPanBegin_.bind(this)); this.eventSource_.addEventListener('updatepan', this.onPanUpdate_.bind(this)); this.eventSource_.addEventListener('endpan', this.onPanEnd_.bind(this)); this.eventSource_.addEventListener('beginzoom', this.onZoomBegin_.bind(this)); this.eventSource_.addEventListener('updatezoom', this.onZoomUpdate_.bind(this)); this.eventSource_.addEventListener('endzoom', this.onZoomEnd_.bind(this)); this.eventSource_.addEventListener('beginrotate', this.onRotateBegin_.bind(this)); this.eventSource_.addEventListener('updaterotate', this.onRotateUpdate_.bind(this)); this.eventSource_.addEventListener('endrotate', this.onRotateEnd_.bind(this)); this.listeners_ = {}; this.eye_ = [0, 0, -500]; this.scale_ = constants.DEFAULT_SCALE; this.rotation_ = [0, 0]; this.pixelRatio_ = window.devicePixelRatio || 1; }, get modelViewMatrix() { var viewportRect_ = base.windowRectForElement(this.canvas_).scaleSize(this.pixelRatio_); var mvMatrix = mat4.create(); // Translate modelView by the eye. mat4.translate(mvMatrix, mvMatrix, this.eye_); // Figure out around which point to rotate (considering scale). var x = (this.deviceRect_.left + this.deviceRect_.right) / 2; var y = (this.deviceRect_.top + this.deviceRect_.bottom) / 2; var p = [x * this.scale_, y * this.scale_, 0, 1]; // Compute the rotation matrix. var rotation = mat4.create(); mat4.rotate(rotation, rotation, this.rotation_[0], [1, 0, 0]); mat4.rotate(rotation, rotation, this.rotation_[1], [0, 1, 0]); // See where the original p is taken by the rotation matrix. var newP = [0, 0, 0]; vec4.transformMat4(newP, p, rotation); // Figure out where to translate so that the rotation point stays // stationary. var delta = [p[0] - newP[0], p[1] - newP[1]]; // Apply the translation. mat4.translate(mvMatrix, mvMatrix, [delta[0], delta[1], 0]); // Finally apply the rotation matrix itself. mat4.multiply(mvMatrix, mvMatrix, rotation); // Apply scale. mat4.scale(mvMatrix, mvMatrix, [this.scale_, this.scale_, 1]); return mvMatrix; }, get projectionMatrix() { // TODO(vmpstr): Figure out perspective projection. var rect = base.windowRectForElement(this.canvas_).scaleSize(this.pixelRatio_); var matrix = mat4.create(); mat4.ortho(matrix, 0, rect.width, 0, rect.height, 1, 1000); // NDC to viewport transform. mat4.translate(matrix, matrix, [1, 1, 0]); mat4.scale(matrix, matrix, [rect.width / 2, rect.height / 2, 1]); return matrix; }, get scale() { return this.scale_; }, set canvas(c) { this.canvas_ = c; }, set deviceRect(rect) { this.deviceRect_ = rect; }, resetCamera: function() { this.eye_ = [0, 0, -500]; this.scale_ = constants.DEFAULT_SCALE; this.rotation_ = [0, 0]; if (this.deviceRect_) { var rect = base.windowRectForElement(this.canvas_).scaleSize(this.pixelRatio_); this.scale_ = 0.7 * Math.min(rect.width / this.deviceRect_.width, rect.height / this.deviceRect_.height); this.scale_ = base.clamp( this.scale_, constants.MINIMUM_SCALE, constants.MAXIMUM_SCALE); this.eye_[0] += (rect.width / 2) - this.scale_ * (this.deviceRect_.left + this.deviceRect_.right) / 2; this.eye_[1] += (rect.height / 2) - this.scale_ * (this.deviceRect_.top + this.deviceRect_.bottom) / 2; } this.dispatchRenderEvent_(); }, updatePanByDelta: function(delta) { var rect = base.windowRectForElement(this.canvas_).scaleSize(this.pixelRatio_); this.eye_[0] += delta[0]; this.eye_[1] += delta[1]; var xLimits = [-this.deviceRect_.width * this.scale_, rect.width]; var yLimits = [-this.deviceRect_.height * this.scale_, rect.height]; this.eye_[0] = base.clamp(this.eye_[0], xLimits[0], xLimits[1]); this.eye_[1] = base.clamp(this.eye_[1], yLimits[0], yLimits[1]); this.dispatchRenderEvent_(); }, updateZoomByDelta: function(delta) { // Negative number should map to (0, 1) // and positive should map to (1, ...). var deltaY = delta[1]; deltaY = base.clamp(deltaY, -50, 50); var scale = 1.0 + deltaY / 100.0; var zoomPoint = this.zoomPoint_; var originalScale = this.scale_; var pointOnSurface = [ (zoomPoint[0] - this.eye_[0]) * this.scale_, (zoomPoint[1] - this.eye_[1]) * this.scale_]; // Update scale. this.scale_ = base.clamp(this.scale_ * scale, constants.MINIMUM_SCALE, constants.MAXIMUM_SCALE); // Now see where the zoom point is on the surface with new scale. var newPointOnSurface = [ (zoomPoint[0] - this.eye_[0]) * this.scale_, (zoomPoint[1] - this.eye_[1]) * this.scale_]; // Shift the eye so that the zoom point remains stationary. var moveDelta = [ (pointOnSurface[0] - newPointOnSurface[0]) / originalScale, (pointOnSurface[1] - newPointOnSurface[1]) / originalScale]; this.updatePanByDelta(moveDelta); this.dispatchRenderEvent_(); }, updateRotateByDelta: function(delta) { this.rotation_[0] -= base.deg2rad(delta[1]); this.rotation_[1] += base.deg2rad(delta[0]); var tiltLimitInRad = base.deg2rad(constants.MAXIMUM_TILT); this.rotation_[0] = base.clamp(this.rotation_[0], -tiltLimitInRad, tiltLimitInRad); this.rotation_[1] = base.clamp(this.rotation_[1], -tiltLimitInRad, tiltLimitInRad); this.dispatchRenderEvent_(); }, // Event callbacks. onPanBegin_: function(e) { this.panning_ = true; this.lastMousePosition_ = this.getMousePosition_(e.data); }, onPanUpdate_: function(e) { if (!this.panning_) return; var delta = this.getMouseDelta_(e.data, this.lastMousePosition_); this.lastMousePosition_ = this.getMousePosition_(e.data); this.updatePanByDelta(delta); }, onPanEnd_: function(e) { this.panning_ = false; }, onZoomBegin_: function(e) { this.zooming_ = true; var p = this.getMousePosition_(e.data); this.lastMousePosition_ = p; this.zoomPoint_ = p; }, onZoomUpdate_: function(e) { if (!this.zooming_) return; var delta = this.getMouseDelta_(e.data, this.lastMousePosition_); this.lastMousePosition_ = this.getMousePosition_(e.data); this.updateZoomByDelta(delta); }, onZoomEnd_: function(e) { this.zooming_ = false; this.zoomPoint_ = undefined; }, onRotateBegin_: function(e) { this.rotating_ = true; this.lastMousePosition_ = this.getMousePosition_(e.data); }, onRotateUpdate_: function(e) { if (!this.rotating_) return; var delta = this.getMouseDelta_(e.data, this.lastMousePosition_); this.lastMousePosition_ = this.getMousePosition_(e.data); this.updateRotateByDelta(delta); }, onRotateEnd_: function(e) { this.rotating_ = false; }, // Misc helper functions. getMousePosition_: function(data) { var rect = base.windowRectForElement(this.canvas_); return [(data.clientX - rect.x) * this.pixelRatio_, (data.clientY - rect.y) * this.pixelRatio_]; }, getMouseDelta_: function(data, p) { var newP = this.getMousePosition_(data); return [newP[0] - p[0], newP[1] - p[1]]; }, dispatchRenderEvent_: function() { base.dispatchSimpleEvent(this, 'renderrequired', false, false); } }; return { Camera: Camera }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('base.guid'); base.exportTo('base', function() { /** * KeyEventManager avoids leaks when listening for keys. * * A common but leaky pattern is: * document.addEventListener('key*', function().bind(this)) * This leaks. * * Instead do this: * KeyEventManager.instance.addListener('keyDown', func, this); * * This will not leak. BUT, note, if "this" is not attached to the document, * it will NOT receive input events. * * Conceptually, KeyEventManager works by making the this refrence "weak", * which is actually accomplished by putting a guid on the thisArg. When keys * are received, we look for elements with that guid and dispatch the keys to * them. */ function KeyEventManager(opt_document) { this.document_ = opt_document || document; if (KeyEventManager.instance) throw new Error('KeyEventManager is a singleton.'); this.onEvent_ = this.onEvent_.bind(this); this.document_.addEventListener('keydown', this.onEvent_); this.document_.addEventListener('keypress', this.onEvent_); this.document_.addEventListener('keyup', this.onEvent_); this.listeners_ = []; } KeyEventManager.instance = undefined; KeyEventManager.resetInstanceForUnitTesting = function() { if (KeyEventManager.instance) { KeyEventManager.instance.destroy(); KeyEventManager.instance = undefined; } KeyEventManager.instance = new KeyEventManager(); } KeyEventManager.prototype = { addListener: function(type, handler, thisArg) { if (!thisArg.keyEventManagerGuid_) { thisArg.keyEventManagerGuid_ = base.GUID.allocate(); thisArg.keyEventManagerRefCount_ = 0; } thisArg.classList.add('key-event-manager-target'); thisArg.keyEventManagerRefCount_++; var guid = thisArg.keyEventManagerGuid_; this.listeners_.push({ guid: guid, type: type, handler: handler }); }, onEvent_: function(event) { // This does standard DOM event propagation of the given event, but using // guids to locate the thisArg for each listener. See event_target.js for // notes on how this works. var preventDefaultState = undefined; var stopPropagationCalled = false; var oldPreventDefault = event.preventDefault; event.preventDefault = function() { preventDefaultState = false; oldPreventDefault.call(this); }; var oldStopPropagation = event.stopPropagation; event.stopPropagation = function() { stopPropagationCalled = true; oldStopPropagation.call(this); }; event.stopImmediatePropagation = function() { throw new Error('Not implemented'); }; var possibleThisArgs = this.document_.querySelectorAll( '.key-event-manager-target'); var possibleThisArgsByGUID = {}; for (var i = 0; i < possibleThisArgs.length; i++) { possibleThisArgsByGUID[possibleThisArgs[i].keyEventManagerGuid_] = possibleThisArgs[i]; } // We need to copy listeners_ and verify the thisArgs exists on each loop // iteration because the event callbacks can change the DOM and listener // list. var listeners = this.listeners_.concat(); var type = event.type; var prevented = 0; for (var i = 0; i < listeners.length; i++) { var listener = listeners[i]; if (listener.type !== type) continue; // thisArg went away. var thisArg = possibleThisArgsByGUID[listener.guid]; if (!thisArg) continue; var handler = listener.handler; if (handler.handleEvent) prevented |= handler.handleEvent.call(handler, event) === false; else prevented |= handler.call(thisArg, event) === false; if (stopPropagationCalled) break; } // We want to return false if preventDefaulted, or one of the handlers // return false. But otherwise, we want to return undefiend. return !prevented && preventDefaultState; }, removeListener: function(type, handler, thisArg) { if (thisArg.keyEventManagerGuid_ === undefined) throw new Error('Was not registered with KeyEventManager'); if (thisArg.keyEventManagerRefCount_ === 0) throw new Error('No events were registered on the provided thisArg'); for (var i = 0; i < this.listeners_.length; i++) { var listener = this.listeners_[i]; if (listener.type == type && listener.handler == handler && listener.guid == thisArg.keyEventManagerGuid_) { thisArg.keyEventManagerRefCount_--; if (thisArg.keyEventManagerRefCount_ === 0) thisArg.classList.remove('key-event-manager-target'); this.listeners_.splice(i, 1); return; } } throw new Error('Listener not found'); }, destroy: function() { this.listeners_.splice(0); this.document_.removeEventListener('keydown', this.onEvent_); this.document_.removeEventListener('keypress', this.onEvent_); this.document_.removeEventListener('keyup', this.onEvent_); }, dispatchFakeEvent: function(type, args) { var e = new KeyboardEvent(type, args); return KeyEventManager.instance.onEvent_.call(undefined, e); } }; KeyEventManager.instance = new KeyEventManager(); return { KeyEventManager: KeyEventManager }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.exportTo('tracing', function() { var constants = { HEADING_WIDTH: 250, MIN_MOUSE_SELECTION_DISTANCE: 4, LEFT_MOUSE_BUTTON: 0 }; return { constants: constants }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview A Mouse-event abtraction that waits for * mousedown, then watches for subsequent mousemove events * until the next mouseup event, then waits again. * State changes are signaled with * 'mouse-tracker-start' : mousedown and tracking * 'mouse-tracker-move' : mouse move * 'mouse-tracker-end' : mouseup and not tracking. */ base.exportTo('ui', function() { /** * @constructor * @param {HTMLElement} targetElement will recv events 'mouse-tracker-start', * 'mouse-tracker-move', 'mouse-tracker-end'. */ function MouseTracker(opt_targetElement) { this.onMouseDown_ = this.onMouseDown_.bind(this); this.onMouseMove_ = this.onMouseMove_.bind(this); this.onMouseUp_ = this.onMouseUp_.bind(this); this.targetElement = opt_targetElement; } MouseTracker.prototype = { get targetElement() { return this.targetElement_; }, set targetElement(targetElement) { if (this.targetElement_) this.targetElement_.removeEventListener('mousedown', this.onMouseDown_); this.targetElement_ = targetElement; if (this.targetElement_) this.targetElement_.addEventListener('mousedown', this.onMouseDown_); }, onMouseDown_: function(e) { if (e.button !== 0) return true; e = this.remakeEvent_(e, 'mouse-tracker-start'); this.targetElement_.dispatchEvent(e); document.addEventListener('mousemove', this.onMouseMove_); document.addEventListener('mouseup', this.onMouseUp_); this.targetElement_.addEventListener('blur', this.onMouseUp_); this.savePreviousUserSelect_ = document.body.style['-webkit-user-select']; document.body.style['-webkit-user-select'] = 'none'; e.preventDefault(); return true; }, onMouseMove_: function(e) { e = this.remakeEvent_(e, 'mouse-tracker-move'); this.targetElement_.dispatchEvent(e); }, onMouseUp_: function(e) { document.removeEventListener('mousemove', this.onMouseMove_); document.removeEventListener('mouseup', this.onMouseUp_); this.targetElement_.removeEventListener('blur', this.onMouseUp_); document.body.style['-webkit-user-select'] = this.savePreviousUserSelect_; e = this.remakeEvent_(e, 'mouse-tracker-end'); this.targetElement_.dispatchEvent(e); }, remakeEvent_: function(e, newType) { var remade = new base.Event(newType, true, true); remade.x = e.x; remade.y = e.y; remade.offsetX = e.offsetX; remade.offsetY = e.offsetY; remade.clientX = e.clientX; remade.clientY = e.clientY; return remade; } }; function trackMouseMovesUntilMouseUp(mouseMoveHandler, opt_mouseUpHandler) { function cleanupAndDispatchToMouseUp(e) { document.removeEventListener('mousemove', mouseMoveHandler); document.removeEventListener('mouseup', cleanupAndDispatchToMouseUp); if (opt_mouseUpHandler) opt_mouseUpHandler(e); } document.addEventListener('mousemove', mouseMoveHandler); document.addEventListener('mouseup', cleanupAndDispatchToMouseUp); } return { MouseTracker: MouseTracker, trackMouseMovesUntilMouseUp: trackMouseMovesUntilMouseUp }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireStylesheet('ui.tool_button'); base.requireStylesheet('ui.mouse_mode_selector'); base.requireTemplate('ui.mouse_mode_selector'); base.require('base.events'); base.require('tracing.constants'); base.require('base.events'); base.require('base.iteration_helpers'); base.require('base.utils'); base.require('base.key_event_manager'); base.require('ui'); base.require('ui.mouse_tracker'); base.exportTo('ui', function() { var MOUSE_SELECTOR_MODE = {}; MOUSE_SELECTOR_MODE.SELECTION = 0x1; MOUSE_SELECTOR_MODE.PANSCAN = 0x2; MOUSE_SELECTOR_MODE.ZOOM = 0x4; MOUSE_SELECTOR_MODE.TIMING = 0x8; MOUSE_SELECTOR_MODE.ROTATE = 0x10; MOUSE_SELECTOR_MODE.ALL_MODES = 0x1F; var allModeInfo = {}; allModeInfo[MOUSE_SELECTOR_MODE.PANSCAN] = { title: 'pan', className: 'pan-scan-mode-button', eventNames: { enter: 'enterpan', begin: 'beginpan', update: 'updatepan', end: 'endpan', exit: 'exitpan' } }; allModeInfo[MOUSE_SELECTOR_MODE.SELECTION] = { title: 'selection', className: 'selection-mode-button', eventNames: { enter: 'enterselection', begin: 'beginselection', update: 'updateselection', end: 'endselection', exit: 'exitselection' } }; allModeInfo[MOUSE_SELECTOR_MODE.ZOOM] = { title: 'zoom', className: 'zoom-mode-button', eventNames: { enter: 'enterzoom', begin: 'beginzoom', update: 'updatezoom', end: 'endzoom', exit: 'exitzoom' } }; allModeInfo[MOUSE_SELECTOR_MODE.TIMING] = { title: 'timing', className: 'timing-mode-button', eventNames: { enter: 'entertiming', begin: 'begintiming', update: 'updatetiming', end: 'endtiming', exit: 'exittiming' } }; allModeInfo[MOUSE_SELECTOR_MODE.ROTATE] = { title: 'rotate', className: 'rotate-mode-button', eventNames: { enter: 'enterrotate', begin: 'beginrotate', update: 'updaterotate', end: 'endrotate', exit: 'exitrotate' } }; var MODIFIER = { SHIFT: 0x1, SPACE: 0x2, CMD_OR_CTRL: 0x4 }; /** * Provides a panel for switching the interaction mode of the mouse. * It handles the user interaction and dispatches events for the various * modes. * * @constructor * @extends {HTMLDivElement} */ var MouseModeSelector = ui.define('div'); MouseModeSelector.prototype = { __proto__: HTMLDivElement.prototype, decorate: function(opt_targetElement) { this.classList.add('mouse-mode-selector'); var node = base.instantiateTemplate('#mouse-mode-selector-template'); this.appendChild(node); this.buttonsEl_ = this.querySelector('.buttons'); this.dragHandleEl_ = this.querySelector('.drag-handle'); this.supportedModeMask = MOUSE_SELECTOR_MODE.ALL_MODES; this.initialRelativeMouseDownPos_ = {x: 0, y: 0}; this.defaultMode_ = MOUSE_SELECTOR_MODE.PANSCAN; this.settingsKey_ = undefined; this.mousePos_ = {x: 0, y: 0}; this.mouseDownPos_ = {x: 0, y: 0}; this.dragHandleEl_.addEventListener('mousedown', this.onDragHandleMouseDown_.bind(this)); this.onMouseDown_ = this.onMouseDown_.bind(this); this.onMouseMove_ = this.onMouseMove_.bind(this); this.onMouseUp_ = this.onMouseUp_.bind(this); this.buttonsEl_.addEventListener('mouseup', this.onButtonMouseUp_); this.buttonsEl_.addEventListener('mousedown', this.onButtonMouseDown_); this.buttonsEl_.addEventListener('click', this.onButtonPress_.bind(this)); base.KeyEventManager.instance.addListener( 'keydown', this.onKeyDown_, this); base.KeyEventManager.instance.addListener( 'keyup', this.onKeyUp_, this); this.mode_ = undefined; this.modeToKeyCodeMap_ = {}; this.modifierToModeMap_ = {}; this.targetElement = opt_targetElement; this.modeBeforeAlternativeModeActivated_ = null; this.exitAlternativeModeModifier_ = null; this.isInteracting_ = false; this.isClick_ = false; }, set targetElement(target) { if (this.targetElement_) this.targetElement_.removeEventListener('mousedown', this.onMouseDown_); this.targetElement_ = target; if (this.targetElement_) this.targetElement_.addEventListener('mousedown', this.onMouseDown_); }, get defaultMode() { return this.defaultMode_; }, set defaultMode(defaultMode) { this.defaultMode_ = defaultMode; }, get settingsKey() { return this.settingsKey_; }, set settingsKey(settingsKey) { this.settingsKey_ = settingsKey; if (!this.settingsKey_) return; var mode = base.Settings.get(this.settingsKey_ + '.mode', undefined); // Modes changed from 1,2,3,4 to 0x1, 0x2, 0x4, 0x8. Fix any stray // settings to the best of our abilities. if (allModeInfo[mode] === undefined) mode = undefined; // Restoring settings against unsupported modes should just go back to the // default mode. if ((mode & this.supportedModeMask_) === 0) mode = undefined; if (!mode) mode = this.defaultMode_; this.mode = mode; var pos = base.Settings.get(this.settingsKey_ + '.pos', undefined); if (pos) this.pos = pos; }, get supportedModeMask() { return this.supportedModeMask_; }, /** * Sets the supported modes. Should be an OR-ing of MOUSE_SELECTOR_MODE * values. */ set supportedModeMask(supportedModeMask) { if (this.mode && (supportedModeMask & this.mode) === 0) throw new Error('supportedModeMask must include current mode.'); function createButtonForMode(mode) { var button = document.createElement('div'); button.mode = mode; button.title = allModeInfo[mode].title; button.classList.add('tool-button'); button.classList.add(allModeInfo[mode].className); return button; } this.supportedModeMask_ = supportedModeMask; this.buttonsEl_.textContent = ''; for (var modeName in MOUSE_SELECTOR_MODE) { if (modeName == 'ALL_MODES') continue; var mode = MOUSE_SELECTOR_MODE[modeName]; if ((this.supportedModeMask_ & mode) === 0) continue; this.buttonsEl_.appendChild(createButtonForMode(mode)); } }, get mode() { return this.currentMode_; }, set mode(newMode) { if (newMode !== undefined) { if (typeof newMode !== 'number') throw new Error('Mode must be a number'); if ((newMode & this.supportedModeMask_) === 0) throw new Error('Cannot switch to this mode, it is not supported'); if (allModeInfo[newMode] === undefined) throw new Error('Unrecognized mode'); } var modeInfo; if (this.currentMode_ === newMode) return; if (this.currentMode_) { modeInfo = allModeInfo[this.currentMode_]; var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className); if (buttonEl) buttonEl.classList.remove('active'); if (!this.isInAlternativeMode_) base.dispatchSimpleEvent(this, modeInfo.eventNames.exit, true); } this.currentMode_ = newMode; if (this.currentMode_) { modeInfo = allModeInfo[this.currentMode_]; var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className); if (buttonEl) buttonEl.classList.add('active'); if (!this.isInAlternativeMode_) base.dispatchSimpleEvent(this, modeInfo.eventNames.enter, true); } if (this.settingsKey_ && !this.isInAlternativeMode_) base.Settings.set(this.settingsKey_ + '.mode', this.mode); }, setKeyCodeForMode: function(mode, keyCode) { if ((mode & this.supportedModeMask_) === 0) throw new Error('Mode not supported'); this.modeToKeyCodeMap_[mode] = keyCode; if (!this.buttonsEl_) return; var modeInfo = allModeInfo[mode]; var buttonEl = this.buttonsEl_.querySelector('.' + modeInfo.className); if (buttonEl) { buttonEl.title = modeInfo.title + ' (' + String.fromCharCode(keyCode) + ')'; } }, setPositionFromEvent_: function(pos, e) { pos.x = e.clientX; pos.y = e.clientY; }, onMouseDown_: function(e) { if (e.button !== tracing.constants.LEFT_MOUSE_BUTTON) return; this.setPositionFromEvent_(this.mouseDownPos_, e); var mouseEvent = new base.Event( allModeInfo[this.mode].eventNames.begin, true); mouseEvent.data = e; this.dispatchEvent(mouseEvent); this.isInteracting_ = true; this.isClick_ = true; ui.trackMouseMovesUntilMouseUp(this.onMouseMove_, this.onMouseUp_); }, onMouseMove_: function(e) { this.setPositionFromEvent_(this.mousePos_, e); var mouseEvent = new base.Event( allModeInfo[this.mode].eventNames.update, true); mouseEvent.data = e; mouseEvent.deltaX = e.x - this.mouseDownPos_.x; mouseEvent.deltaY = e.y - this.mouseDownPos_.y; mouseEvent.mouseDownPosition = this.mouseDownPos_; this.dispatchEvent(mouseEvent); if (this.isInteracting_) this.checkIsClick_(e); }, onMouseUp_: function(e) { if (e.button !== tracing.constants.LEFT_MOUSE_BUTTON) return; var mouseEvent = new base.Event( allModeInfo[this.mode].eventNames.end, true); mouseEvent.data = e; mouseEvent.consumed = false; mouseEvent.isClick = this.isClick_; this.dispatchEvent(mouseEvent); if (this.isClick_ && !mouseEvent.consumed) this.dispatchClickEvents_(e); this.isInteracting_ = false; }, onButtonMouseDown_: function(e) { e.preventDefault(); e.stopImmediatePropagation(); }, onButtonMouseUp_: function(e) { e.preventDefault(); e.stopImmediatePropagation(); }, onButtonPress_: function(e) { this.setAlternateMode_(null); this.mode = e.target.mode; e.preventDefault(); }, onKeyDown_: function(e) { // Prevent the user from changing modes during an interaction. if (this.isInteracting_) return; if (this.isInAlternativeMode_) return; var modifierToModeMap = this.modifierToModeMap_; var mode = this.mode; var m = MODIFIER; var modifier; var shiftPressed = e.shiftKey; var spacePressed = e.keyCode === ' '.charCodeAt(0); var cmdOrCtrlPressed = (base.isMac && e.metaKey) || (!base.isMac && e.ctrlKey); if (shiftPressed && modifierToModeMap[m.SHIFT] !== mode) modifier = m.SHIFT; else if (spacePressed && modifierToModeMap[m.SPACE] !== mode) modifier = m.SPACE; else if (cmdOrCtrlPressed && modifierToModeMap[m.CMD_OR_CTRL] !== mode) modifier = m.CMD_OR_CTRL; else return; this.setAlternateMode_(modifier); }, onKeyUp_: function(e) { // Prevent the user from changing modes during an interaction. if (this.isInteracting_) return; var didHandleKey = false; base.iterItems(this.modeToKeyCodeMap_, function(modeStr, keyCode) { if (e.keyCode === keyCode) { this.setAlternateMode_(null); var mode = parseInt(modeStr); this.mode = mode; didHandleKey = true; } }, this); if (didHandleKey) { e.preventDefault(); e.stopPropagation(); return; } if (!this.isInAlternativeMode_) return; var shiftReleased = !e.shiftKey; var spaceReleased = e.keyCode === ' '.charCodeAt(0); var cmdOrCtrlReleased = (base.isMac && !e.metaKey) || (!base.isMac && !e.ctrlKey); var exitModifier = this.exitAlternativeModeModifier_; if ((shiftReleased && exitModifier === MODIFIER.SHIFT) || (spaceReleased && exitModifier === MODIFIER.SPACE) || (cmdOrCtrlReleased && exitModifier === MODIFIER.CMD_OR_CTRL)) { this.setAlternateMode_(null); } }, get isInAlternativeMode_() { return !!this.modeBeforeAlternativeModeActivated_; }, setAlternateMode_: function(modifier) { if (!modifier) { if (this.isInAlternativeMode_) { this.mode = this.modeBeforeAlternativeModeActivated_; this.modeBeforeAlternativeModeActivated_ = null; } return; } var alternateMode = this.modifierToModeMap_[modifier]; if ((alternateMode & this.supportedModeMask_) === 0) return; this.modeBeforeAlternativeModeActivated_ = this.mode; this.exitAlternativeModeModifier_ = modifier; this.mode = alternateMode; }, setModifierForAlternateMode: function(mode, modifier) { this.modifierToModeMap_[modifier] = mode; }, get pos() { return { x: parseInt(this.style.left), y: parseInt(this.style.top) }; }, set pos(pos) { pos = this.constrainPositionToBounds_(pos); this.style.left = pos.x + 'px'; this.style.top = pos.y + 'px'; if (this.settingsKey_) base.Settings.set(this.settingsKey_ + '.pos', this.pos); }, constrainPositionToBounds_: function(pos) { var parent = this.offsetParent || document.body; var parentRect = base.windowRectForElement(parent); var top = 0; var bottom = parentRect.height - this.offsetHeight; var left = 0; var right = parentRect.width - this.offsetWidth; var res = {}; res.x = Math.max(pos.x, left); res.x = Math.min(res.x, right); res.y = Math.max(pos.y, top); res.y = Math.min(res.y, bottom); return res; }, onDragHandleMouseDown_: function(e) { e.preventDefault(); e.stopImmediatePropagation(); this.initialRelativeMouseDownPos_.x = e.clientX - this.offsetLeft; this.initialRelativeMouseDownPos_.y = e.clientY - this.offsetTop; ui.trackMouseMovesUntilMouseUp(this.onDragHandleMouseMove_.bind(this)); }, onDragHandleMouseMove_: function(e) { var pos = {}; pos.x = (e.clientX - this.initialRelativeMouseDownPos_.x); pos.y = (e.clientY - this.initialRelativeMouseDownPos_.y); this.pos = pos; }, checkIsClick_: function(e) { if (!this.isInteracting_ || !this.isClick_) return; var deltaX = this.mousePos_.x - this.mouseDownPos_.x; var deltaY = this.mousePos_.y - this.mouseDownPos_.y; var minDist = tracing.constants.MIN_MOUSE_SELECTION_DISTANCE; if (deltaX * deltaX + deltaY * deltaY > minDist * minDist) this.isClick_ = false; }, dispatchClickEvents_: function(e) { if (!this.isClick_) return; var eventNames = allModeInfo[MOUSE_SELECTOR_MODE.SELECTION].eventNames; var mouseEvent = new base.Event(eventNames.begin, true); mouseEvent.data = e; this.dispatchEvent(mouseEvent); mouseEvent = new base.Event(eventNames.end, true); mouseEvent.data = e; this.dispatchEvent(mouseEvent); } }; return { MouseModeSelector: MouseModeSelector, MOUSE_SELECTOR_MODE: MOUSE_SELECTOR_MODE, MODIFIER: MODIFIER }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview QuadStackView controls the content and viewing angle a * QuadStack. */ base.requireStylesheet('ui.quad_stack_view'); base.requireTemplate('ui.quad_stack_view'); base.require('base.raf'); base.require('ui.camera'); base.require('ui.mouse_mode_selector'); base.require('ui.mouse_tracker'); base.exportTo('ui', function() { var constants = {}; constants.IMAGE_LOAD_RETRY_TIME_MS = 500; // Care of bckenney@ via // http://extremelysatisfactorytotalitarianism.com/blog/?p=2120 function drawTexturedTriangle( ctx, img, x0, y0, x1, y1, x2, y2, u0, v0, u1, v1, u2, v2) { ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.closePath(); x1 -= x0; y1 -= y0; x2 -= x0; y2 -= y0; u1 -= u0; v1 -= v0; u2 -= u0; v2 -= v0; var det = 1 / (u1 * v2 - u2 * v1), // linear transformation a = (v2 * x1 - v1 * x2) * det, b = (v2 * y1 - v1 * y2) * det, c = (u1 * x2 - u2 * x1) * det, d = (u1 * y2 - u2 * y1) * det, // translation e = x0 - a * u0 - c * v0, f = y0 - b * u0 - d * v0; ctx.save(); ctx.transform(a, b, c, d, e, f); ctx.clip(); ctx.drawImage(img, 0, 0); ctx.restore(); } // Created to avoid creating garbage when doing bulk transforms. var tmp_vec4 = vec4.create(); function transform(transformed, point, matrix) { vec4.set(tmp_vec4, point[0], point[1], 0, 1); vec4.transformMat4(tmp_vec4, tmp_vec4, matrix); transformed[0] = tmp_vec4[0] / tmp_vec4[3]; transformed[1] = tmp_vec4[1] / tmp_vec4[3]; } function drawProjectedQuadBackgroundToContext( quad, p1, p2, p3, p4, ctx, quadCanvas) { if (quad.imageData) { quadCanvas.width = quad.imageData.width; quadCanvas.height = quad.imageData.height; quadCanvas.getContext('2d').putImageData(quad.imageData, 0, 0); ctx.save(); var quadBBox = new base.BBox2(); quadBBox.addQuad(quad); var iw = quadCanvas.width; var ih = quadCanvas.height; drawTexturedTriangle( ctx, quadCanvas, p1[0], p1[1], p2[0], p2[1], p4[0], p4[1], 0, 0, iw, 0, 0, ih); drawTexturedTriangle( ctx, quadCanvas, p2[0], p2[1], p3[0], p3[1], p4[0], p4[1], iw, 0, iw, ih, 0, ih); ctx.restore(); } if (quad.backgroundColor) { ctx.fillStyle = quad.backgroundColor; ctx.beginPath(); ctx.moveTo(p1[0], p1[1]); ctx.lineTo(p2[0], p2[1]); ctx.lineTo(p3[0], p3[1]); ctx.lineTo(p4[0], p4[1]); ctx.closePath(); ctx.fill(); } } function drawProjectedQuadOutlineToContext( quad, p1, p2, p3, p4, ctx, quadCanvas) { ctx.beginPath(); ctx.moveTo(p1[0], p1[1]); ctx.lineTo(p2[0], p2[1]); ctx.lineTo(p3[0], p3[1]); ctx.lineTo(p4[0], p4[1]); ctx.closePath(); ctx.save(); if (quad.borderColor) ctx.strokeStyle = quad.borderColor; else ctx.strokeStyle = 'rgb(128,128,128)'; if (quad.shadowOffset) { ctx.shadowColor = 'rgb(0, 0, 0)'; ctx.shadowOffsetX = quad.shadowOffset[0]; ctx.shadowOffsetY = quad.shadowOffset[1]; if (quad.shadowBlur) ctx.shadowBlur = quad.shadowBlur; } if (quad.borderWidth) ctx.lineWidth = quad.borderWidth; else ctx.lineWidth = 1; ctx.stroke(); ctx.restore(); } function drawProjectedQuadSelectionOutlineToContext( quad, p1, p2, p3, p4, ctx, quadCanvas) { if (!quad.upperBorderColor) return; ctx.lineWidth = 8; ctx.strokeStyle = quad.upperBorderColor; ctx.beginPath(); ctx.moveTo(p1[0], p1[1]); ctx.lineTo(p2[0], p2[1]); ctx.lineTo(p3[0], p3[1]); ctx.lineTo(p4[0], p4[1]); ctx.closePath(); ctx.stroke(); } function drawProjectedQuadToContext( passNumber, quad, p1, p2, p3, p4, ctx, quadCanvas) { if (passNumber === 0) { drawProjectedQuadBackgroundToContext( quad, p1, p2, p3, p4, ctx, quadCanvas); } else if (passNumber === 1) { drawProjectedQuadOutlineToContext( quad, p1, p2, p3, p4, ctx, quadCanvas); } else if (passNumber === 2) { drawProjectedQuadSelectionOutlineToContext( quad, p1, p2, p3, p4, ctx, quadCanvas); } else { throw new Error('Invalid pass number'); } } var tmp_p1 = vec2.create(); var tmp_p2 = vec2.create(); var tmp_p3 = vec2.create(); var tmp_p4 = vec2.create(); function transformAndProcessQuads( matrix, quads, numPasses, handleQuadFunc, opt_arg1, opt_arg2) { for (var passNumber = 0; passNumber < numPasses; passNumber++) { for (var i = 0; i < quads.length; i++) { var quad = quads[i]; transform(tmp_p1, quad.p1, matrix); transform(tmp_p2, quad.p2, matrix); transform(tmp_p3, quad.p3, matrix); transform(tmp_p4, quad.p4, matrix); handleQuadFunc(passNumber, quad, tmp_p1, tmp_p2, tmp_p3, tmp_p4, opt_arg1, opt_arg2); } } } /** * @constructor */ var QuadStackView = ui.define('quad-stack-view'); QuadStackView.prototype = { __proto__: HTMLUnknownElement.prototype, decorate: function() { this.className = 'quad-stack-view'; var node = base.instantiateTemplate('#quad-stack-view-template'); this.appendChild(node); this.canvas_ = this.querySelector('#canvas'); this.chromeImages_ = { left: this.querySelector('#chrome-left'), mid: this.querySelector('#chrome-mid'), right: this.querySelector('#chrome-right') }; this.trackMouse_(); this.camera_ = new ui.Camera(this.mouseModeSelector_); this.camera_.addEventListener('renderrequired', this.onRenderRequired_.bind(this)); this.cameraWasReset_ = false; this.camera_.canvas = this.canvas_; this.viewportRect_ = base.Rect.fromXYWH(0, 0, 0, 0); this.stackingDistance_ = 45; this.pixelRatio_ = window.devicePixelRatio || 1; }, onStackingDistanceChange: function(e) { this.stackingDistance_ = parseInt(e.target.value); this.scheduleRender(); }, get mouseModeSelector() { return this.mouseModeSelector_; }, get camera() { return this.camera_; }, set quads(q) { this.quads_ = q; this.scheduleRender(); }, set deviceRect(rect) { if (!rect || rect.equalTo(this.deviceRect_)) return; this.deviceRect_ = rect; this.camera_.deviceRect = rect; this.chromeQuad_ = undefined; }, resize: function() { if (!this.offsetParent) return true; var width = parseInt(window.getComputedStyle(this.offsetParent).width); var height = parseInt(window.getComputedStyle(this.offsetParent).height); var rect = base.Rect.fromXYWH(0, 0, width, height); if (rect.equalTo(this.viewportRect_)) return false; this.viewportRect_ = rect; this.style.width = width + 'px'; this.style.height = height + 'px'; this.canvas_.style.width = width + 'px'; this.canvas_.style.height = height + 'px'; this.canvas_.width = this.pixelRatio_ * width; this.canvas_.height = this.pixelRatio_ * height; if (!this.cameraWasReset_) { this.camera_.resetCamera(); this.cameraWasReset_ = true; } return true; }, readyToDraw: function() { // If src isn't set yet, set it to ensure we can use // the image to draw onto a canvas. if (!this.chromeImages_.left.src) { var leftContent = window.getComputedStyle(this.chromeImages_.left).content; leftContent = leftContent.replace(/url\((.*)\)/, '$1'); var midContent = window.getComputedStyle(this.chromeImages_.mid).content; midContent = midContent.replace(/url\((.*)\)/, '$1'); var rightContent = window.getComputedStyle(this.chromeImages_.right).content; rightContent = rightContent.replace(/url\((.*)\)/, '$1'); this.chromeImages_.left.src = leftContent; this.chromeImages_.mid.src = midContent; this.chromeImages_.right.src = rightContent; } // If all of the images are loaded (height > 0), then // we are ready to draw. return (this.chromeImages_.left.height > 0) && (this.chromeImages_.mid.height > 0) && (this.chromeImages_.right.height > 0); }, get chromeQuad() { if (this.chromeQuad_) return this.chromeQuad_; // Draw the chrome border into a separate canvas. var chromeCanvas = document.createElement('canvas'); var offsetY = this.chromeImages_.left.height; chromeCanvas.width = this.deviceRect_.width; chromeCanvas.height = this.deviceRect_.height + offsetY; var leftWidth = this.chromeImages_.left.width; var midWidth = this.chromeImages_.mid.width; var rightWidth = this.chromeImages_.right.width; var chromeCtx = chromeCanvas.getContext('2d'); chromeCtx.drawImage(this.chromeImages_.left, 0, 0); chromeCtx.save(); chromeCtx.translate(leftWidth, 0); // Calculate the scale of the mid image. var s = (this.deviceRect_.width - leftWidth - rightWidth) / midWidth; chromeCtx.scale(s, 1); chromeCtx.drawImage(this.chromeImages_.mid, 0, 0); chromeCtx.restore(); chromeCtx.drawImage( this.chromeImages_.right, leftWidth + s * midWidth, 0); // Construct the quad. var chromeRect = base.Rect.fromXYWH( this.deviceRect_.x, this.deviceRect_.y - offsetY, this.deviceRect_.width, this.deviceRect_.height + offsetY); var chromeQuad = base.Quad.fromRect(chromeRect); chromeQuad.stackingGroupId = this.maxStachingGroupId_ + 1; chromeQuad.imageData = chromeCtx.getImageData( 0, 0, chromeCanvas.width, chromeCanvas.height); chromeQuad.shadowOffset = [0, 0]; chromeQuad.shadowBlur = 5; chromeQuad.borderWidth = 3; this.chromeQuad_ = chromeQuad; return this.chromeQuad_; }, scheduleRender: function() { if (this.redrawScheduled_) return false; this.redrawScheduled_ = true; base.requestAnimationFrame(this.render, this); }, onRenderRequired_: function(e) { this.scheduleRender(); }, stackTransformAndProcessQuads_: function( numPasses, handleQuadFunc, includeChromeQuad, opt_arg1, opt_arg2) { var mv = this.camera_.modelViewMatrix; var p = this.camera_.projectionMatrix; // Calculate the quad stacks. var quadStacks = []; for (var i = 0; i < this.quads_.length; ++i) { var quad = this.quads_[i]; var stackingId = quad.stackingGroupId; while (stackingId >= quadStacks.length) quadStacks.push([]); quadStacks[stackingId].push(quad); } var mvp = mat4.create(); this.maxStackingGroupId_ = quadStacks.length; var stackingDistance = this.stackingDistance_ * this.camera_.scale; // Draw the quad stacks, raising each subsequent level. mat4.multiply(mvp, p, mv); for (var i = 0; i < quadStacks.length; ++i) { transformAndProcessQuads(mvp, quadStacks[i], numPasses, handleQuadFunc, opt_arg1, opt_arg2); mat4.translate(mv, mv, [0, 0, stackingDistance]); mat4.multiply(mvp, p, mv); } if (includeChromeQuad) { transformAndProcessQuads(mvp, [this.chromeQuad], numPasses, drawProjectedQuadToContext, opt_arg1, opt_arg2); } }, render: function() { this.redrawScheduled_ = false; if (!this.readyToDraw()) { setTimeout(this.scheduleRender.bind(this), constants.IMAGE_LOAD_RETRY_TIME_MS); return; } if (!this.quads_) return; var canvasCtx = this.canvas_.getContext('2d'); if (!this.resize()) canvasCtx.clearRect(0, 0, this.canvas_.width, this.canvas_.height); var quadCanvas = document.createElement('canvas'); this.stackTransformAndProcessQuads_( 3, drawProjectedQuadToContext, true, canvasCtx, quadCanvas); quadCanvas.width = 0; // Hack: Frees the quadCanvas' resources. var fontSize = parseInt(15 * this.pixelRatio_); canvasCtx.font = fontSize + 'px Arial'; canvasCtx.fillStyle = 'rgb(0, 0, 0)'; canvasCtx.fillText( 'Scale: ' + parseInt(this.camera_.scale * 100) + '%', 10, fontSize); }, trackMouse_: function() { this.mouseModeSelector_ = new ui.MouseModeSelector(this); this.mouseModeSelector_.supportedModeMask = ui.MOUSE_SELECTOR_MODE.SELECTION | ui.MOUSE_SELECTOR_MODE.PANSCAN | ui.MOUSE_SELECTOR_MODE.ZOOM | ui.MOUSE_SELECTOR_MODE.ROTATE; this.mouseModeSelector_.mode = ui.MOUSE_SELECTOR_MODE.PANSCAN; this.mouseModeSelector_.pos = {x: 0, y: 100}; this.appendChild(this.mouseModeSelector_); this.mouseModeSelector_.settingsKey = 'quadStackView.mouseModeSelector'; this.mouseModeSelector_.setModifierForAlternateMode( ui.MOUSE_SELECTOR_MODE.ROTATE, ui.MODIFIER.SHIFT); this.mouseModeSelector_.setModifierForAlternateMode( ui.MOUSE_SELECTOR_MODE.PANSCAN, ui.MODIFIER.SPACE); this.mouseModeSelector_.setModifierForAlternateMode( ui.MOUSE_SELECTOR_MODE.ZOOM, ui.MODIFIER.CMD_OR_CTRL); this.mouseModeSelector_.addEventListener('updateselection', this.onSelectionUpdate_.bind(this)); this.mouseModeSelector_.addEventListener('endselection', this.onSelectionUpdate_.bind(this)); }, extractRelativeMousePosition_: function(e) { var br = this.canvas_.getBoundingClientRect(); return [ this.pixelRatio_ * (e.data.clientX - this.canvas_.offsetLeft - br.left), this.pixelRatio_ * (e.data.clientY - this.canvas_.offsetTop - br.top) ]; }, onSelectionUpdate_: function(e) { var mousePos = this.extractRelativeMousePosition_(e); var res = []; function handleQuad(passNumber, quad, p1, p2, p3, p4) { if (base.pointInImplicitQuad(mousePos, p1, p2, p3, p4)) res.push(quad); } this.stackTransformAndProcessQuads_(1, handleQuad, false); var e = new Event('selectionchange', false, false); e.quads = res; this.dispatchEvent(e); } }; return { QuadStackView: QuadStackView }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview Graphical view of LayerTreeImpl, with controls for * type of layer content shown and info bar for content-loading warnings. */ base.requireStylesheet('cc.layer_tree_quad_stack_view'); base.require('base.color'); base.require('base.properties'); base.require('base.raf'); base.require('cc.constants'); base.require('cc.picture'); base.require('cc.tile'); base.require('cc.debug_colors'); base.require('ui.quad_stack_view'); base.require('ui.info_bar'); base.exportTo('cc', function() { var TILE_HEATMAP_TYPE = {}; TILE_HEATMAP_TYPE.NONE = 0; TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY = 1; TILE_HEATMAP_TYPE.DISTANCE_TO_VISIBLE = 2; TILE_HEATMAP_TYPE.TIME_TO_VISIBLE = 3; function createTileRectsSelectorBaseOptions() { return [{label: 'None', value: 'none'}, {label: 'Coverage Rects', value: 'coverage'}]; } /** * @constructor */ var LayerTreeQuadStackView = ui.define('layer-tree-quad-stack-view'); LayerTreeQuadStackView.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { this.pictureAsImageData_ = {}; // Maps picture.guid to PictureAsImageData. this.messages_ = []; this.controls_ = document.createElement('top-controls'); this.infoBar_ = new ui.InfoBar(); this.quadStackView_ = new ui.QuadStackView(); this.quadStackView_.addEventListener( 'selectionchange', this.onQuadStackViewSelectionChange_.bind(this)); var m = ui.MOUSE_SELECTOR_MODE; var mms = this.quadStackView_.mouseModeSelector; mms.settingsKey = 'cc.layerTreeQuadStackView.mouseModeSelector'; mms.setKeyCodeForMode(m.SELECTION, 'Z'.charCodeAt(0)); mms.setKeyCodeForMode(m.PANSCAN, 'X'.charCodeAt(0)); mms.setKeyCodeForMode(m.ZOOM, 'C'.charCodeAt(0)); mms.setKeyCodeForMode(m.ROTATE, 'V'.charCodeAt(0)); this.appendChild(this.controls_); this.appendChild(this.infoBar_); this.appendChild(this.quadStackView_); this.tileRectsSelector_ = ui.createSelector( this, 'howToShowTiles', 'layerView.howToShowTiles', 'none', createTileRectsSelectorBaseOptions()); this.controls_.appendChild(this.tileRectsSelector_); var tileHeatmapText = ui.createSpan({ textContent: 'Tile heatmap:' }); this.controls_.appendChild(tileHeatmapText); var tileHeatmapSelector = ui.createSelector( this, 'tileHeatmapType', 'layerView.tileHeatmapType', TILE_HEATMAP_TYPE.NONE, [{label: 'None', value: TILE_HEATMAP_TYPE.NONE}, {label: 'Scheduled Priority', value: TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY}, {label: 'Distance to Visible', value: TILE_HEATMAP_TYPE.DISTANCE_TO_VISIBLE}, {label: 'Time to Visible', value: TILE_HEATMAP_TYPE.TIME_TO_VISIBLE} ]); this.controls_.appendChild(tileHeatmapSelector); var showOtherLayersCheckbox = ui.createCheckBox( this, 'showOtherLayers', 'layerView.showOtherLayers', true, 'Other layers'); showOtherLayersCheckbox.title = 'When checked, show all layers, selected or not.'; this.controls_.appendChild(showOtherLayersCheckbox); var showInvalidationsCheckbox = ui.createCheckBox( this, 'showInvalidations', 'layerView.showInvalidations', true, 'Invalidations'); showInvalidationsCheckbox.title = 'When checked, compositing invalidations are highlighted in red'; this.controls_.appendChild(showInvalidationsCheckbox); var showBottlenecksCheckbox = ui.createCheckBox( this, 'showBottlenecks', 'layerView.showBottlenecks', true, 'Bottlenecks'); showBottlenecksCheckbox.title = 'When checked, scroll bottlenecks are highlighted'; this.controls_.appendChild(showBottlenecksCheckbox); var showContentsCheckbox = ui.createCheckBox( this, 'showContents', 'layerView.showContents', true, 'Contents'); showContentsCheckbox.title = 'When checked, show the rendered contents inside the layer outlines'; this.controls_.appendChild(showContentsCheckbox); }, get layerTreeImpl() { return this.layerTreeImpl_; }, set layerTreeImpl(layerTreeImpl) { // FIXME(pdr): We may want to clear pictureAsImageData_ here to save // memory at the cost of performance. Note that // pictureAsImageData_ will be cleared when this is // destructed, but this view might live for several // layerTreeImpls. this.layerTreeImpl_ = layerTreeImpl; this.selection = undefined; this.updateTilesSelector_(); this.updateContents_(); }, get showOtherLayers() { return this.showOtherLayers_; }, set showOtherLayers(show) { this.showOtherLayers_ = show; this.updateContents_(); }, get showContents() { return this.showContents_; }, set showContents(show) { this.showContents_ = show; this.updateContents_(); }, get showInvalidations() { return this.showInvalidations_; }, set showInvalidations(show) { this.showInvalidations_ = show; this.updateContents_(); }, get showBottlenecks() { return this.showBottlenecks_; }, set showBottlenecks(show) { this.showBottlenecks_ = show; this.updateContents_(); }, get howToShowTiles() { return this.howToShowTiles_; }, set howToShowTiles(val) { // Make sure val is something we expect. console.assert( (val === 'none') || (val === 'coverage') || !isNaN(parseFloat(val))); this.howToShowTiles_ = val; this.updateContents_(); }, get tileHeatmapType() { return this.tileHeatmapType_; }, set tileHeatmapType(val) { this.tileHeatmapType_ = val; this.updateContents_(); }, get selection() { return this.selection_; }, set selection(selection) { base.setPropertyAndDispatchChange(this, 'selection', selection); this.updateContents_(); }, onQuadStackViewSelectionChange_: function(e) { var selectableQuads = e.quads.filter(function(q) { return q.selectionToSetIfClicked !== undefined; }); if (selectableQuads.length == 0) { this.selection = undefined; return; } // Sort the quads low to high on stackingGroupId. selectableQuads.sort(function(x, y) { var z = x.stackingGroupId - y.stackingGroupId; if (z != 0) return z; return x.selectionToSetIfClicked.specicifity - y.selectionToSetIfClicked.specicifity; }); // TODO(nduca): Support selecting N things at once. var quadToSelect = selectableQuads[selectableQuads.length - 1]; this.selection = quadToSelect.selectionToSetIfClicked; }, scheduleUpdateContents_: function() { if (this.updateContentsPending_) return; this.updateContentsPending_ = true; base.requestAnimationFrameInThisFrameIfPossible( this.updateContents_, this); }, updateContents_: function() { if (!this.layerTreeImpl_) return; var status = this.computePictureLoadingStatus_(); if (!status.picturesComplete) return; var lthi = this.layerTreeImpl_.layerTreeHostImpl; var lthiInstance = lthi.objectInstance; var worldViewportRect = base.Rect.fromXYWH( 0, 0, lthi.deviceViewportSize.width, lthi.deviceViewportSize.height); this.quadStackView_.deviceRect = worldViewportRect; this.quadStackView_.quads = this.generateQuads(); this.updateInfoBar_(status.messages); }, updateTilesSelector_: function() { var data = createTileRectsSelectorBaseOptions(); // First get all of the scales information from LTHI. var lthi = this.layerTreeImpl_.layerTreeHostImpl; var scaleNames = lthi.getContentsScaleNames(); for (var scale in scaleNames) { data.push({ label: 'Scale ' + scale + ' (' + scaleNames[scale] + ')', value: scale }); } // Then create a new selector and replace the old one. var new_selector = ui.createSelector( this, 'howToShowTiles', 'layerView.howToShowTiles', 'none', data); this.controls_.replaceChild(new_selector, this.tileRectsSelector_); this.tileRectsSelector_ = new_selector; }, computePictureLoadingStatus_: function() { // Figure out if we can draw the quads yet. While we're at it, figure out // if we have any warnings we need to show. var layers = this.layers; var status = { messages: [], picturesComplete: true }; if (this.showContents) { var hasPendingRasterizeImage = false; var firstPictureError = undefined; var hasMissingLayerRect = false; var hasUnresolvedPictureRef = false; for (var i = 0; i < layers.length; i++) { var layer = layers[i]; for (var ir = layer.pictures.length - 1; ir >= 0; ir--) { var picture = layer.pictures[ir]; if (picture.idRef) { hasUnresolvedPictureRef = true; continue; } if (!picture.layerRect) { hasMissingLayerRect = true; continue; } var pictureAsImageData = this.pictureAsImageData_[picture.guid]; if (!pictureAsImageData) { hasPendingRasterizeImage = true; this.pictureAsImageData_[picture.guid] = cc.PictureAsImageData.Pending(this); picture.rasterize( {stopIndex: undefined}, function(pictureImageData) { var picture_ = pictureImageData.picture; this.pictureAsImageData_[picture_.guid] = pictureImageData; this.scheduleUpdateContents_(); }.bind(this)); continue; } if (pictureAsImageData.isPending()) { hasPendingRasterizeImage = true; continue; } if (pictureAsImageData.error) { if (!firstPictureError) firstPictureError = pictureAsImageData.error; break; } } } if (hasPendingRasterizeImage) { status.picturesComplete = false; } else { if (hasUnresolvedPictureRef) { status.messages.push({ header: 'Missing picture', details: 'Your trace didnt have pictures for every layer. ' + 'Old chrome versions had this problem'}); } if (hasMissingLayerRect) { status.messages.push({ header: 'Missing layer rect', details: 'Your trace may be corrupt or from a very old ' + 'Chrome revision.'}); } if (firstPictureError) { status.messages.push({ header: 'Cannot rasterize', details: firstPictureError}); } } } return status; }, get selectedLayer() { if (this.selection) { var selectedLayerId = this.selection.associatedLayerId; return this.layerTreeImpl_.findLayerWithId(selectedLayerId); } }, get layers() { var layers = this.layerTreeImpl.renderSurfaceLayerList; if (!this.showOtherLayers) { var selectedLayer = this.selectedLayer; if (selectedLayer) layers = [selectedLayer]; } return layers; }, appendImageQuads_: function(quads, layer, layerQuad) { // Generate image quads for the layer for (var ir = layer.pictures.length - 1; ir >= 0; ir--) { var picture = layer.pictures[ir]; if (!picture.layerRect) continue; var unitRect = picture.layerRect.asUVRectInside(layer.bounds); var iq = layerQuad.projectUnitRect(unitRect); var pictureData = this.pictureAsImageData_[picture.guid]; if (this.showContents && pictureData && pictureData.imageData) { iq.imageData = pictureData.imageData; iq.borderColor = 'rgba(0,0,0,0)'; } else { iq.imageData = undefined; } iq.stackingGroupId = layerQuad.stackingGroupId; quads.push(iq); } }, appendInvalidationQuads_: function(quads, layer, layerQuad) { // Generate the invalidation rect quads. for (var ir = 0; ir < layer.invalidation.rects.length; ir++) { var rect = layer.invalidation.rects[ir]; var unitRect = rect.asUVRectInside(layer.bounds); var iq = layerQuad.projectUnitRect(unitRect); iq.backgroundColor = 'rgba(255, 0, 0, 0.1)'; iq.borderColor = 'rgba(255, 0, 0, 1)'; iq.stackingGroupId = layerQuad.stackingGroupId; iq.selectionToSetIfClicked = new cc.LayerRectSelection( layer, 'Invalidation rect', rect, rect); quads.push(iq); } }, appendBottleneckQuads_: function(quads, layer, layerQuad, stackingGroupId) { function processRegion(region, label, borderColor) { var backgroundColor = borderColor.clone(); backgroundColor.a = 0.4 * (borderColor.a || 1.0); for (var ir = 0; ir < region.rects.length; ir++) { var rect = region.rects[ir]; var unitRect = rect.asUVRectInside(layer.bounds); var iq = layerQuad.projectUnitRect(unitRect); iq.backgroundColor = backgroundColor.toString(); iq.borderColor = borderColor.toString(); iq.borderWidth = 4.0; iq.stackingGroupId = stackingGroupId; iq.selectionToSetIfClicked = new cc.LayerRectSelection( layer, label, rect, rect); quads.push(iq); } } processRegion(layer.touchEventHandlerRegion, 'Touch listener', base.Color.fromString('rgb(228, 226, 27)')); processRegion(layer.wheelEventHandlerRegion, 'Wheel listener', base.Color.fromString('rgb(176, 205, 29)')); processRegion(layer.nonFastScrollableRegion, 'Repaints on scroll', base.Color.fromString('rgb(213, 134, 32)')); }, appendTileCoverageRectQuads_: function( quads, layer, layerQuad, heatmapType) { if (!layer.tileCoverageRects) return; var tiles = []; for (var ct = 0; ct < layer.tileCoverageRects.length; ++ct) { var tile = layer.tileCoverageRects[ct].tile; if (tile !== undefined) tiles.push(tile); } var heatmapColors = this.computeHeatmapColors_(tiles, heatmapType); var heatIndex = 0; for (var ct = 0; ct < layer.tileCoverageRects.length; ++ct) { var rect = layer.tileCoverageRects[ct].geometryRect; rect = rect.scale(1.0 / layer.geometryContentsScale); var tile = layer.tileCoverageRects[ct].tile; var unitRect = rect.asUVRectInside(layer.bounds); var quad = layerQuad.projectUnitRect(unitRect); quad.backgroundColor = 'rgba(0, 0, 0, 0)'; quad.stackingGroupId = layerQuad.stackingGroupId; var type = cc.tileTypes.missing; if (tile) { type = tile.getTypeForLayer(layer); quad.backgroundColor = heatmapColors[heatIndex]; ++heatIndex; } quad.borderColor = cc.tileBorder[type].color; quad.borderWidth = cc.tileBorder[type].width; var label; if (tile) label = 'coverageRect'; else label = 'checkerboard coverageRect'; quad.selectionToSetIfClicked = new cc.LayerRectSelection( layer, label, rect, layer.tileCoverageRects[ct]); quads.push(quad); } }, getValueForHeatmap_: function(tile, heatmapType) { if (heatmapType == TILE_HEATMAP_TYPE.SCHEDULED_PRIORITY) return tile.scheduledPriority; else if (heatmapType == TILE_HEATMAP_TYPE.DISTANCE_TO_VISIBLE) return tile.distanceToVisible; else if (heatmapType == TILE_HEATMAP_TYPE.TIME_TO_VISIBLE) return tile.timeToVisible; }, computeHeatmapColors_: function(tiles, heatmapType) { var maxValue = 0; for (var i = 0; i < tiles.length; ++i) { var tile = tiles[i]; var value = this.getValueForHeatmap_(tile, heatmapType); if (value !== undefined) maxValue = Math.max(value, maxValue); } if (maxValue == 0) maxValue = 1; var color = function(value) { var hue = 120 * (1 - value / maxValue); if (hue < 0) hue = 0; return 'hsla(' + hue + ', 100%, 50%, 0.5)'; }; var values = []; for (var i = 0; i < tiles.length; ++i) { var tile = tiles[i]; var value = this.getValueForHeatmap_(tile, heatmapType); if (value !== undefined) values.push(color(value)); else values.push(undefined); } return values; }, appendTilesWithScaleQuads_: function( quads, layer, layerQuad, scale, heatmapType) { var lthi = this.layerTreeImpl_.layerTreeHostImpl; var tiles = []; for (var i = 0; i < lthi.tiles.length; ++i) { var tile = lthi.tiles[i]; if (Math.abs(tile.contentsScale - scale) > 1e-6) continue; // TODO(vmpstr): Make the stiching of tiles and layers a part of // tile construction (issue 346) if (layer.layerId != tile.layerId) continue; tiles.push(tile); } var heatmapColors = this.computeHeatmapColors_(tiles, heatmapType); for (var i = 0; i < tiles.length; ++i) { var tile = tiles[i]; var rect = tile.layerRect; if (!tile.layerRect) continue; var unitRect = rect.asUVRectInside(layer.bounds); var quad = layerQuad.projectUnitRect(unitRect); quad.backgroundColor = 'rgba(0, 0, 0, 0)'; quad.stackingGroupId = layerQuad.stackingGroupId; var type = tile.getTypeForLayer(layer); quad.borderColor = cc.tileBorder[type].color; quad.borderWidth = cc.tileBorder[type].width; quad.backgroundColor = heatmapColors[i]; quad.selectionToSetIfClicked = new cc.TileSelection(tile); quads.push(quad); } }, appendSelectionQuads_: function(quads, layer, layerQuad) { var selection = this.selection; var rect = selection.layerRect; if (!rect) return []; var unitRect = rect.asUVRectInside(layer.bounds); var quad = layerQuad.projectUnitRect(unitRect); var colorId = tracing.getStringColorId(selection.title); colorId += tracing.getColorPaletteHighlightIdBoost(); var color = base.Color.fromString(tracing.getColorPalette()[colorId]); var quadForDrawing = quad.clone(); quadForDrawing.backgroundColor = color.withAlpha(0.5).toString(); quadForDrawing.borderColor = color.withAlpha(1.0).darken().toString(); quadForDrawing.stackingGroupId = layerQuad.stackingGroupId; quads.push(quadForDrawing); }, generateQuads: function() { this.updateContentsPending_ = false; // Generate the quads for the view. var layers = this.layers; var quads = []; var nextStackingGroupId = 0; var alreadyVisitedLayerIds = {}; for (var i = 0; i < layers.length; i++) { var layer = layers[i]; alreadyVisitedLayerIds[layer.layerId] = true; if (layer.objectInstance.name == 'cc::NinePatchLayerImpl') continue; var layerQuad = layer.layerQuad.clone(); layerQuad.borderColor = 'rgba(0,0,0,0.75)'; layerQuad.stackingGroupId = nextStackingGroupId++; layerQuad.selectionToSetIfClicked = new cc.LayerSelection(layer); layerQuad.layer = layer; if (this.showOtherLayers && this.selectedLayer == layer) layerQuad.upperBorderColor = 'rgb(156,189,45)'; this.appendImageQuads_(quads, layer, layerQuad); quads.push(layerQuad); if (this.showInvalidations) this.appendInvalidationQuads_(quads, layer, layerQuad); if (this.showBottlenecks) this.appendBottleneckQuads_(quads, layer, layerQuad, layerQuad.stackingGroupId); if (this.howToShowTiles === 'coverage') { this.appendTileCoverageRectQuads_( quads, layer, layerQuad, this.tileHeatmapType); } else if (this.howToShowTiles !== 'none') { this.appendTilesWithScaleQuads_( quads, layer, layerQuad, this.howToShowTiles, this.tileHeatmapType); } if (this.selectedLayer === layer) this.appendSelectionQuads_(quads, layer, layerQuad); } this.layerTreeImpl.iterLayers(function(layer, depth, isMask, isReplica) { if (!this.showOtherLayers && this.selectedLayer != layer) return; if (alreadyVisitedLayerIds[layer.layerId]) return; var layerQuad = layer.layerQuad; var stackingGroupId = nextStackingGroupId++; if (this.showBottlenecks) this.appendBottleneckQuads_(quads, layer, layerQuad, stackingGroupId); }, this); return quads; }, updateInfoBar_: function(infoBarMessages) { if (infoBarMessages.length) { this.infoBar_.removeAllButtons(); this.infoBar_.message = 'Some problems were encountered...'; this.infoBar_.addButton('More info...', function(e) { var overlay = new ui.Overlay(); overlay.textContent = ''; infoBarMessages.forEach(function(message) { var title = document.createElement('h3'); title.textContent = message.header; var details = document.createElement('div'); details.textContent = message.details; overlay.appendChild(title); overlay.appendChild(details); }); overlay.visible = true; e.stopPropagation(); return false; }); this.infoBar_.visible = true; } else { this.infoBar_.removeAllButtons(); this.infoBar_.message = ''; this.infoBar_.visible = false; } } }; return { LayerTreeQuadStackView: LayerTreeQuadStackView }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; /** * @fileoverview LayerView coordinates graphical and analysis views of layers. */ base.requireStylesheet('cc.layer_view'); base.require('base.raf'); base.require('base.settings'); base.require('cc.constants'); base.require('cc.layer_tree_quad_stack_view'); base.require('tracing.analysis.util'); base.require('ui.drag_handle'); base.exportTo('cc', function() { var constants = cc.constants; /** * @constructor */ var LayerView = ui.define('layer-view'); LayerView.prototype = { __proto__: HTMLUnknownElement.prototype, decorate: function() { this.layerTreeQuadStackView_ = new cc.LayerTreeQuadStackView(); this.dragBar_ = new ui.DragHandle(); this.analysisEl_ = document.createElement('layer-view-analysis'); this.dragBar_.target = this.analysisEl_; this.appendChild(this.layerTreeQuadStackView_); this.appendChild(this.dragBar_); this.appendChild(this.analysisEl_); this.layerTreeQuadStackView_.addEventListener('selectionChange', function() { this.layerTreeQuadStackViewSelectionChanged_(); }.bind(this)); this.layerTreeQuadStackViewSelectionChanged_(); }, get layerTreeImpl() { return this.layerTreeQuadStackView_.layerTreeImpl; }, set layerTreeImpl(newValue) { return this.layerTreeQuadStackView_.layerTreeImpl = newValue; }, get selection() { return this.layerTreeQuadStackView_.selection; }, set selection(newValue) { this.layerTreeQuadStackView_.selection = newValue; }, layerTreeQuadStackViewSelectionChanged_: function() { var selection = this.layerTreeQuadStackView_.selection; if (selection) { this.dragBar_.style.display = ''; this.analysisEl_.style.display = ''; this.analysisEl_.textContent = ''; var analysis = selection.createAnalysis(); this.analysisEl_.appendChild(analysis); } else { this.dragBar_.style.display = 'none'; this.analysisEl_.style.display = 'none'; var analysis = this.analysisEl_.firstChild; if (analysis) this.analysisEl_.removeChild(analysis); this.layerTreeQuadStackView_.style.height = window.getComputedStyle(this).height; } } }; return { LayerView: LayerView }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.require('ui'); base.exportTo('tracing.analysis', function() { var ObjectSnapshotView = ui.define('object-snapshot-view'); ObjectSnapshotView.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { this.objectSnapshot_ = undefined; }, get requiresTallView() { return true; }, set modelEvent(obj) { this.objectSnapshot = obj; }, get modelEvent() { return this.objectSnapshot; }, get objectSnapshot() { return this.objectSnapshot_; }, set objectSnapshot(i) { this.objectSnapshot_ = i; this.updateContents(); }, updateContents: function() { throw new Error('Not implemented'); } }; ObjectSnapshotView.typeNameToViewInfoMap = {}; ObjectSnapshotView.register = function(typeName, viewConstructor, opt_options) { if (ObjectSnapshotView.typeNameToViewInfoMap[typeName]) throw new Error('Handler already registered for ' + typeName); var options = opt_options || { showInTrackView: true }; ObjectSnapshotView.typeNameToViewInfoMap[typeName] = { constructor: viewConstructor, options: options }; }; ObjectSnapshotView.unregister = function(typeName) { if (ObjectSnapshotView.typeNameToViewInfoMap[typeName] === undefined) throw new Error(typeName + ' not registered'); delete ObjectSnapshotView.typeNameToViewInfoMap[typeName]; }; ObjectSnapshotView.getViewInfo = function(typeName) { return ObjectSnapshotView.typeNameToViewInfoMap[typeName]; }; return { ObjectSnapshotView: ObjectSnapshotView }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireStylesheet('cc.layer_tree_host_impl_view'); base.require('cc.layer_tree_host_impl'); base.require('cc.layer_picker'); base.require('cc.layer_view'); base.require('cc.tile'); base.require('tracing.analysis.object_snapshot_view'); base.require('ui.drag_handle'); base.exportTo('cc', function() { /* * Displays a LayerTreeHostImpl snapshot in a human readable form. * @constructor */ var LayerTreeHostImplSnapshotView = ui.define( 'layer-tree-host-impl-snapshot-view', tracing.analysis.ObjectSnapshotView); LayerTreeHostImplSnapshotView.prototype = { __proto__: tracing.analysis.ObjectSnapshotView.prototype, decorate: function() { this.classList.add('lthi-s-view'); this.selection_ = undefined; this.layerPicker_ = new cc.LayerPicker(); this.layerPicker_.addEventListener( 'selection-changed', this.onLayerPickerSelectionChanged_.bind(this)); this.layerView_ = new cc.LayerView(); this.layerView_.addEventListener( 'selection-changed', this.onLayerViewSelectionChanged_.bind(this)); this.dragHandle_ = new ui.DragHandle(); this.dragHandle_.horizontal = false; this.dragHandle_.target = this.layerView_; this.appendChild(this.layerPicker_); this.appendChild(this.dragHandle_); this.appendChild(this.layerView_); }, get objectSnapshot() { return this.objectSnapshot_; }, set objectSnapshot(objectSnapshot) { this.objectSnapshot_ = objectSnapshot; var lthi = this.objectSnapshot; var layerTreeImpl; if (lthi) layerTreeImpl = lthi.getTree(this.layerPicker_.whichTree); this.layerPicker_.lthiSnapshot = lthi; this.layerView_.layerTreeImpl = layerTreeImpl; if (!this.selection_) return; this.selection = this.selection_.findEquivalent(lthi); }, get selection() { return this.selection_; }, set selection(selection) { this.selection_ = selection; this.layerPicker_.selection = selection; this.layerView_.selection = selection; }, onLayerPickerSelectionChanged_: function() { this.selection_ = this.layerPicker_.selection; this.layerView_.selection = this.selection; }, onLayerViewSelectionChanged_: function() { this.selection_ = this.layerView_.selection; this.layerPicker_.selection = this.selection; } }; tracing.analysis.ObjectSnapshotView.register( 'cc::LayerTreeHostImpl', LayerTreeHostImplSnapshotView); return { LayerTreeHostImplSnapshotView: LayerTreeHostImplSnapshotView }; }); // Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. 'use strict'; base.requireStylesheet('cc.picture_ops_chart_summary_view'); base.exportTo('cc', function() { var OPS_TIMING_ITERATIONS = 3; var CHART_PADDING_LEFT = 65; var CHART_PADDING_RIGHT = 40; var AXIS_PADDING_LEFT = 60; var AXIS_PADDING_RIGHT = 35; var AXIS_PADDING_TOP = 25; var AXIS_PADDING_BOTTOM = 45; var AXIS_LABEL_PADDING = 5; var AXIS_TICK_SIZE = 10; var LABEL_PADDING = 5; var LABEL_INTERLEAVE_OFFSET = 15; var BAR_PADDING = 5; var VERTICAL_TICKS = 5; var HUE_CHAR_CODE_ADJUSTMENT = 5.7; /** * Provides a chart showing the cumulative time spent in Skia operations * during picture rasterization. * * @constructor */ var PictureOpsChartSummaryView = ui.define('picture-ops-chart-summary-view'); PictureOpsChartSummaryView.prototype = { __proto__: HTMLUnknownElement.prototype, decorate: function() { this.picture_ = undefined; this.pictureDataProcessed_ = false; this.chartScale_ = window.devicePixelRatio; this.chart_ = document.createElement('canvas'); this.chartCtx_ = this.chart_.getContext('2d'); this.appendChild(this.chart_); this.opsTimingData_ = []; this.chartWidth_ = 0; this.chartHeight_ = 0; this.requiresRedraw_ = true; this.currentBarMouseOverTarget_ = null; this.chart_.addEventListener('mousemove', this.onMouseMove_.bind(this)); }, get requiresRedraw() { return this.requiresRedraw_; }, set requiresRedraw(requiresRedraw) { this.requiresRedraw_ = requiresRedraw; }, get picture() { return this.picture_; }, set picture(picture) { this.picture_ = picture; this.pictureDataProcessed_ = false; if (this.classList.contains('hidden')) return; this.processPictureData_(); this.requiresRedraw = true; this.updateChartContents(); }, hide: function() { this.classList.add('hidden'); }, show: function() { this.classList.remove('hidden'); if (this.pictureDataProcessed_) return; this.processPictureData_(); this.requiresRedraw = true; this.updateChartContents(); }, onMouseMove_: function(e) { var lastBarMouseOverTarget = this.currentBarMouseOverTarget_; this.currentBarMouseOverTarget_ = null; var x = e.offsetX; var y = e.offsetY; var chartLeft = CHART_PADDING_LEFT; var chartRight = this.chartWidth_ - CHART_PADDING_RIGHT; var chartTop = AXIS_PADDING_TOP; var chartBottom = this.chartHeight_ - AXIS_PADDING_BOTTOM; var chartInnerWidth = chartRight - chartLeft; if (x > chartLeft && x < chartRight && y > chartTop && y < chartBottom) { this.currentBarMouseOverTarget_ = Math.floor( (x - chartLeft) / chartInnerWidth * this.opsTimingData_.length); this.currentBarMouseOverTarget_ = base.clamp( this.currentBarMouseOverTarget_, 0, this.opsTimingData_.length - 1); } if (this.currentBarMouseOverTarget_ === lastBarMouseOverTarget) return; this.drawChartContents_(); }, updateChartContents: function() { if (this.requiresRedraw) this.updateChartDimensions_(); this.drawChartContents_(); }, updateChartDimensions_: function() { this.chartWidth_ = this.offsetWidth; this.chartHeight_ = this.offsetHeight; // Scale up the canvas according to the devicePixelRatio, then reduce it // down again via CSS. Finally we apply a scale to the canvas so that // things are drawn at the correct size. this.chart_.width = this.chartWidth_ * this.chartScale_; this.chart_.height = this.chartHeight_ * this.chartScale_; this.chart_.style.width = this.chartWidth_ + 'px'; this.chart_.style.height = this.chartHeight_ + 'px'; this.chartCtx_.scale(this.chartScale_, this.chartScale_); }, processPictureData_: function() { this.resetOpsTimingData_(); this.pictureDataProcessed_ = true; if (!this.picture_) return; var ops = this.picture_.getOps(); if (!ops) return; ops = this.picture_.tagOpsWithTimings(ops); // Check that there are valid times. if (ops[0].cmd_time === undefined) return; this.collapseOpsToTimingBuckets_(ops); }, drawChartContents_: function() { this.clearChartContents_(); if (this.opsTimingData_.length === 0) { this.showNoTimingDataMessage_(); return; } this.drawChartAxes_(); this.drawBars_(); this.drawLineAtBottomOfChart_(); if (this.currentBarMouseOverTarget_ === null) return; this.drawTooltip_(); }, drawLineAtBottomOfChart_: function() { this.chartCtx_.strokeStyle = '#AAA'; this.chartCtx_.moveTo(0, this.chartHeight_ - 0.5); this.chartCtx_.lineTo(this.chartWidth_, this.chartHeight_ - 0.5); this.chartCtx_.stroke(); }, drawTooltip_: function() { var tooltipData = this.opsTimingData_[this.currentBarMouseOverTarget_]; var tooltipTitle = tooltipData.cmd_string; var tooltipTime = tooltipData.cmd_time.toFixed(4); var tooltipWidth = 110; var tooltipHeight = 40; var chartInnerWidth = this.chartWidth_ - CHART_PADDING_RIGHT - CHART_PADDING_LEFT; var barWidth = chartInnerWidth / this.opsTimingData_.length; var tooltipOffset = Math.round((tooltipWidth - barWidth) * 0.5); var left = CHART_PADDING_LEFT + this.currentBarMouseOverTarget_ * barWidth - tooltipOffset; var top = Math.round((this.chartHeight_ - tooltipHeight) * 0.5); this.chartCtx_.save(); this.chartCtx_.shadowOffsetX = 0; this.chartCtx_.shadowOffsetY = 5; this.chartCtx_.shadowBlur = 4; this.chartCtx_.shadowColor = 'rgba(0,0,0,0.4)'; this.chartCtx_.strokeStyle = '#888'; this.chartCtx_.fillStyle = '#EEE'; this.chartCtx_.fillRect(left, top, tooltipWidth, tooltipHeight); this.chartCtx_.shadowColor = 'transparent'; this.chartCtx_.translate(0.5, 0.5); this.chartCtx_.strokeRect(left, top, tooltipWidth, tooltipHeight); this.chartCtx_.restore(); this.chartCtx_.fillStyle = '#222'; this.chartCtx_.textBaseline = 'top'; this.chartCtx_.font = '800 12px Arial'; this.chartCtx_.fillText(toolt