From dc6aafecc2b0eb7dc44347051bb1077f3dd79c0d Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 8 Aug 2024 08:28:40 +0200 Subject: [PATCH] Setup tracing and document tracing usage (#12730) --- .gitignore | 8 + Cargo.lock | 22 +- Cargo.toml | 3 +- crates/red_knot/Cargo.toml | 8 +- crates/red_knot/docs/tracing-flamegraph.png | Bin 0 -> 41274 bytes crates/red_knot/docs/tracing.md | 103 +++++++ crates/red_knot/src/logging.rs | 254 ++++++++++++++++++ crates/red_knot/src/main.rs | 123 +++------ crates/red_knot/src/verbosity.rs | 33 --- .../red_knot_module_resolver/src/resolver.rs | 10 +- .../src/semantic_index.rs | 8 +- .../src/types/infer.rs | 6 +- crates/red_knot_workspace/src/lint.rs | 4 +- .../red_knot_workspace/src/site_packages.rs | 4 + .../red_knot_workspace/src/watch/watcher.rs | 7 +- crates/red_knot_workspace/src/workspace.rs | 35 ++- crates/ruff_db/src/files.rs | 17 +- crates/ruff_db/src/files/path.rs | 11 + crates/ruff_db/src/parsed.rs | 2 +- crates/ruff_db/src/source.rs | 2 +- crates/ruff_db/src/vendored/path.rs | 13 + 21 files changed, 513 insertions(+), 160 deletions(-) create mode 100644 crates/red_knot/docs/tracing-flamegraph.png create mode 100644 crates/red_knot/docs/tracing.md create mode 100644 crates/red_knot/src/logging.rs diff --git a/.gitignore b/.gitignore index 4302ff30a7..ad7de8ce76 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,14 @@ flamegraph.svg # `CARGO_TARGET_DIR=target-llvm-lines RUSTFLAGS="-Csymbol-mangling-version=v0" cargo llvm-lines -p ruff --lib` /target* +# samply profiles +profile.json + +# tracing-flame traces +tracing.folded +tracing-flamechart.svg +tracing-flamegraph.svg + ### # Rust.gitignore ### diff --git a/Cargo.lock b/Cargo.lock index 3880a9982a..fb5fbcd0b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1482,11 +1482,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2800e1520bdc966782168a627aa5d1ad92e33b984bf7c7615d31280c83ff14" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1860,7 +1860,9 @@ name = "red_knot" version = "0.0.0" dependencies = [ "anyhow", + "chrono", "clap", + "colored", "countme", "crossbeam", "ctrlc", @@ -1873,6 +1875,7 @@ dependencies = [ "salsa", "tempfile", "tracing", + "tracing-flame", "tracing-subscriber", "tracing-tree", ] @@ -3225,6 +3228,17 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-flame" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bae117ee14789185e129aaee5d93750abe67fdc5a9a62650452bfe4e122a3a9" +dependencies = [ + "lazy_static", + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-indicatif" version = "0.3.6" @@ -3272,7 +3286,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f459ca79f1b0d5f71c54ddfde6debfc59c8b6eeb46808ae492077f739dc7b49c" dependencies = [ - "nu-ansi-term 0.50.0", + "nu-ansi-term 0.50.1", "tracing-core", "tracing-log", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index b9e4e0d625..7ff4b380c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -132,8 +132,9 @@ thiserror = { version = "1.0.58" } tikv-jemallocator = { version = "0.6.0" } toml = { version = "0.8.11" } tracing = { version = "0.1.40" } +tracing-flame = { version = "0.2.0" } tracing-indicatif = { version = "0.3.6" } -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.18", default-features = false, features = ["env-filter", "fmt"] } tracing-tree = { version = "0.4.0" } typed-arena = { version = "2.0.2" } unic-ucd-category = { version = "0.9" } diff --git a/crates/red_knot/Cargo.toml b/crates/red_knot/Cargo.toml index 50781acfd1..5fc0fcf492 100644 --- a/crates/red_knot/Cargo.toml +++ b/crates/red_knot/Cargo.toml @@ -19,20 +19,22 @@ red_knot_server = { workspace = true } ruff_db = { workspace = true, features = ["os", "cache"] } anyhow = { workspace = true } +chrono = { workspace = true } clap = { workspace = true, features = ["wrap_help"] } +colored = { workspace = true } countme = { workspace = true, features = ["enable"] } crossbeam = { workspace = true } ctrlc = { version = "3.4.4" } rayon = { workspace = true } salsa = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } +tracing = { workspace = true, features = ["release_max_level_debug"] } +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } +tracing-flame = { workspace = true } tracing-tree = { workspace = true } [dev-dependencies] filetime = { workspace = true } tempfile = { workspace = true } - [lints] workspace = true diff --git a/crates/red_knot/docs/tracing-flamegraph.png b/crates/red_knot/docs/tracing-flamegraph.png new file mode 100644 index 0000000000000000000000000000000000000000..6a350c6b551ef92e0f683bd4fdf103e6e27859a2 GIT binary patch literal 41274 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 z zP1PP!x%ED*(@-ayZyHix`GRyrDK&ZzLvyqKE|416ql@R5S{Ryq8dCdtqjaW3gxzcH z6EQ|0bJ6TM#B>b`?)nL)Oztm!S}|H?hQ(5jNPR)Ie(TI6zuc7Z%pHRy;(-&LS-Nno zQ7XSU7pq4coSHai@>`c9cPChXxxt}kwY228T{-1I$oQ~RCf`D4_Mhuty=z}4WR&}z zAfCfrPvnTso-nxRVuj&c9wyI+V>>uCs#Yi%@FVT$ncU%f?N2^6ih1kuOyF^^gn`$RzT80LVgoF7I?D z0>AzpA0Lx)${sCfXeT8J2<(=^K`;m(y96ZZ7^G08tb)$Vkbd#dNSABH0m5_=-#`}0 zPw><)Qf^Mmb?ge3TV7P_y(D-CBY!A>cx_?&b^OS;?^%QH`f~?+s6~rKByQ!vXgU^% zw3f$4yRR4w?^x1#ZV6$@;2=Q2kj^FScKq&Cl89!R2IRSUq2ug@U1gIS$n`x)Vz7?1 zztv^vz9{Ic#yk0q51VTMxlrPZxU71tdG}ph=$=IYpk>;70{ao$JP# zK0=LUzG)4HiK6q_?>}twW(pKi{_=1;&6|2Ug%d6xov3?H??tQ-$3I9WL%+?l2JSfQ9m3=l;rM6Tn{(i8&}n^K@NQT zy~(rKS4pyA8=Q=x#?;%0F71s6NuOe>2~q~>mE~pFqG1p*zlpLkx|=*NpVm4v>|Dg_ zH~RqHT|P7_TReIJRQrJvW4n=7PC2puvMogY<8SO+2gh|0_1FbTLxL}^rM)tE*gc8Q z^2Q($yijo3Li_19qsueZ>L@|chAf_6uE`pidx9!#b!E*62G+GcgAH#@Q;3;IT@x*2 za@32T@3Zu%zZ@f=KhKs#Ea)TtfJLZS+}(!Xkt!*C)q>Z$qjcZO<`;j%Yoq>nr3P<0 zI9tp&u*rlbx6AsR0^gx|*Z8z7aPWBszqq0R{;1s(L5&<0ZB?q9Cas&ukDi4Or|kpm z>ZuqE%%eh$>9-KSs`JCFdZ0mpNO|b0NXiNC8D8|hD zzWA<}YaP$%-M5WE+p`o~ix3>aA+x&~E|VtRFlt-P1h~Urn%(V0L&pMV+-Rv>T`K}- z&MRRMuZU60renF*(>k$J-;Jc$ksrp+D{;w33tjEVZrxlXy<1Hkx2v9uzwsrXW*PNs z?NX)PzMeO7>eLRQWNe7#@!;9>D>&{9j3NF4Z~<7qKp!Z2*U>UjQW2W`>6zIyhY?CH zo@c`2-IH(MNI9y1pfwrkJeS~GD6BEJ9{p2+IJz!-fd@oRuQ`+FUis(Em&sfycnKJj0s-=#id=U=8jO);v) zA=A5ocy%mE!M2p`M_t&IXhSZ~SJ!0rODh2tw#G6N#AT@3YGr3mi&M?rMhmEm zl6~zFX_$13!;^YAH&)^`c}hZ#hb`TG?LxDI5wkpnq|i?Vv}1>SjAjbm0}CB0Nk0cs zgjLC5v{1Xtk*|aItFm(LoMuPsk8FUFA|9gSY7TmNuud2z?!{c_iX;$lsmNs2S9jjn<6hSMVyPJmu@O$F+&Xspg_a!(Ed! zzZXVQ)I@fP#Dj=pcyMxW$5|XcrQrkSWSKJLeG#p)3%7&jVBYl6+HdAME$1WySM-Xj*$|eXt(k*aV zZX>#0GQp6(@s()dJ;6LBN7)qNXq{}~@5_N_Drz$JSSsQ#ZK3H(l-rHl(K)WdKXy(V zF`?egwPmx|s+Uoo1kPL2%DjI0vqAS`p0K^^ORec8&*CMQ z2s03f+zxjE&>q*`Gq1_!=j_ANr12>_-t?}-y zN@Df&8eHiHB~{AM(7|zfumOZ?{X}dn^mnQq?ZrD(NAmSauT@)?8CSWf=kT+b=Wz4~ zow?fzRI8O)wVqD2b!Wfi&=!mI5Ym7DGIW&MWvWza{=u9Vr$mUT8r4By@B>TQ!?rGK zTL+wf<8_E&uxMMf=ORAtO<5In#)N~CW*!A@|Kj02<_HN;l zPAZt+e&6s%dd7+ky+By%?`iUP?B`a^3jHSV?wF`1%=Y13b2kUnJOs$`{`KT}o2qm| zC%$I;>ceenM4@w2X%KvAxX`(r3}2U5vTn$JTYkmTc@K5d6(KH?ldT-un^cS6L#G=` z1yW5@B@D~GS&|Sc!dHbId`r`7Qw{8LhvL1zOe;mbS$wV=+fp-RKMN}txVOB0i0P?n zp$kzhV)E~mxfc|r8IWTCXR)%I?et|!(Q>uDr;R>aaFTW;MMzI5nquZs$EnZgLrpIa zQ-Q70cOeMlpH&?XYEQQX-lZ;#4wCM3uCP&NvLo$>E05EJ3|%)hV|b8(N_z)+L_xy- zZnB#k3MsZ-#+u!>%*eSZWc&{CkA;>Zaa1=Pk_ak0vdQ7~f;^cu`c=L~T*5VHzYfl* zU0>mn{PyP0A8GkblC_7qX+x9s^`nG%25@pmB9MK}!6h!9^jS^{ed5g@f`RSlj8WLb zh{ME^*iazS`m{)Lt-rpflWfJ&E5oP+o#!1NKJ-zuAZX3OC8!Vt}6DXW)F2WVLW zENKHC@JhmPP{gauJ!}dM`*2Oe73RQ_ih_SmQS5uD|5zo{S9!2uohRF2zt3ljMs zyRVEn#kdXwW5XqR`uLK-tOaoES-v}q2hLRm9M7Q3KhwFMI=|_)xCrvbg6hiz+W(e+ zKrccu`Y>OJ0vXb^Dk8Hg+6f=x_gwt+FQN+qE9c#`wO+-)K)E30PA4lw0pIcD&6iyM zgPqCW_G6M~oOjaKHT%ryK3w1v@eOb@*KOE ze4M+?6ha4`;Kbl0;$ioaHTTL3Jn| ztu|JlNy-?#bDriW{xWJIDWAP_t`S#vyp`XjQ*c51!(s5~t&|nT_TRY{-jt$1y*?uj zP_Q*OeSdDe&DLR*=;s1^)o&b|m~EjCFyC%a3j;lS^uhgn>La|ChvDMj zr_HpKujSutySPQOn^J3Ops<2>Nv3=ttl+K2^xk8N8S78Mo8sfOqCB*mH$hkgkNgMr zn+zBxWuqG{$Nd%)7=nI?AxtOmXR49zTtSXdi@@^rHQ7buh8j1o?7Z>%O{B{-qplvpGRmKW zu^s|>&+pqb0{yH|hXx-Eq#la26>)9F&!Mc*P0*vU7El15nXBFKPHK`=*gIkFVbmDi z1VxT%?CY})?~cGn7Jz|4!L~=H+P@6Vf|Ljr8P#U0&;TygHoU(L9-p`Ie*@y}|Ltx` z1~q<$Xz*|}wzKV*5Zmml_Mx^hTFXbY{JDbB%0@b*<)+`VF6duNds$H5`=MLlmLvbw-R26f#lsn$%uxQC& z%IkUdi-gMDL>C<^cY4z7(+~`&aSaAfeDLnhQKVm#UMAL4U3K z!+|Kyfw^YD*=af9@!(mON4z;%$sUd#NFEUWsAp-N!6gCwH)NoBF$9=zy0HsL9riOC z+S^Di-=RkeaWYr!OwJHlnBj4-ylmtd&G~kFx#276de5FewlY4Eqm*`dn8_;~x7gfJ zb(5CKTby5H1wwcb2u8z$H6M=NLl)^EV$93K$3$Cx(dcTAF?d(^^M#;f?a(9+s^IW@ zQ=JP5EzGva=P%$><0Xra<0f+IK0S_iY}s0}*vXrCX;K13TFd+bdf$%0$f4gwY%i*#t5h(UZ$A@ zvFemJqX5C51pB$c);zDfw5qrx%M-tMTi-^?X7Yh9ubW)q5)Aax`^Zf@R-JJX)Lnh^ z3c)rc4K%-LzV8mUu#Eg-f=x8JQeQ|;ePPi@L+((9;UJ(wEit6R5qL>1ZAob@7Z;bM z9^5F;kee?xYC};Oji_zGJ;7YT4m^sRLPpNO{AHjLa}!5X!1GV1UDB$=xH8SAffqYi`gv?7ShnRnb}Mmi_P*57B5M6z)1 zMh@%y>x|q8wp&=~2`m&dn<-iAs?%|ZXJ@Sk?B-K#&F@SkfBsHYD@${(Xo+^Entz`V zlkfg1Hcmu|;^*GC`8n~dcw9Lo;Tmc7lg%h1JDN1J{N|c`xJ4xn$sBQ+yr>F}Y&9hv z+6!KD9stR?5ah)%9OM8nrizOfAw=Jb4>KXMq0+)e2G!hpl$2W9q1Qb z3+veDZG1bHTaiE@G$L6~4r#;KF>4nKzeIwykaOBf#;(OrK{MOd+gm+kt`&71YxBmg zdCbfWbgl^=foRp9H>>3B+VJp;aNBn*8S}h6O%TezHNs^3%vWmhrxNK@U@JybzEmpThGIVr=V(&7 z!D3qw<0pM;A~&$nl#gJS$Dk$FD_AOg;=^=*(JyihXB= zvgZgiX1xWG8Mh!Tt4RxQy(Xstooa)5x!l`JxY|k|W>4`MwuSlQobS3Zh|F^_VX;~Oc?sKyuBTnsVKkI!H8a)wBF>|eN-njWm# zXz5z;DuN9YMaBug;h2+t1f?{}s{)VQygsbl7w*Rj%FmUCO2@Sc7#;d|i(nrRuIK7> z$hV+euP@>Z+AKCEC^8Jjj)@1M=7<72HWrjp9f9lZtnd{RKPRM}iWY7ct0oIHk>qR*;UzhAP3@2g!=u##^|4xMD67E!<;Doc-^RW?7H+Z*|^Eo*%hf^Y*Y9nG@ydK&k^# z=21!(z&@E5r=p&7#;f}8`)rQ$lItbbbR=P;8115)jR-5`%;Zj-BJ&iaj z=5=migAQ$tevW>{Mna7oJ;j-?a#ds7N$q~``LmmxdboPoC=M|#FOOHE`~3jWc}(sUiR8rWUO`;)##<$poi?;-tjU`&!7 z+8)#jVj8r3_4Ws)rD%}K*a9v=8jv(6BAK*oVLL4)W$q^ublk;jz891s)faEsk&?aw zL@cZ$Gr$^(q2cHO=Wzy+!`cLK(1t!HkaO9CAzTx`hICH@!ic3>@y0zN><@z2fq!{e zzFe4Ts07X;@HbK?OpJdzpa7YEu=aMaKY${sQ>%EQlW$+WP6C zzgKNs&oxceYfX5)c2P<+JGs3!@kQllz66f4FYdN4HDPVi(@6;>AV*(CS3D1t-nj;k z6N*4e^mzCC6e@5#LdP#Fj~4p<n)S)7qgQYe@e3vu~$dZm6yAqN&e@Oml_^UD97H&m+KU=0Tb{e$&}6|6 zcM#3pJH8>8Vc5q<$3TYVV%OtI3-X>PyNj4eTGHrlq}5h#=Ph}oCS=$AntYGjB0E{P z*Mu#XVx6w>`xDB_$aaILJ76Y0#;NAN%|v4&;i3y=GiCfyR;2B$&=-rW7@9zFsa0gt z^3Ai@R?Pi5N~l>!%TqU_$~zN1yh|Yut@3*eANW!Ci_dCrJ zZry}K89)Q?C-jjK_2V1a`i&4lc3S^Rm_ zdtAanKw6IEjNRQ7QH~$0yNJ_)4b`h4@1>2LXhRa+*~7wps{-^?hj)n2?9r38g4LZh zb1~)f$5J3;wS5;K^@rN4IorqG_}=7%c%d)kqOR84u?C}f7Z+Jv3Zj$_;2`3rC|dh^ z-3@-M5o0nB9*>Q8%-s)uFU)OvwPG!cZ{^y1Vg=qplpJtS9^}b;4qR$NhKj|*bbV*~ zw|X5dBk>_AOV{%ba_!2O6;k`t`-ThSaI(5UAmWGO*qHXL#C-M<>#8a43!wifm-YvA zB12~abBWPn+1&#H*F1sjXHhPiz%?emx@ILgaYV;pA*puQymR9{N3w>Ss-BEAn@|BI zHk1}NQ#ZyXH5(Z-!45*bOUXiF6D|!;5Yjp5O83tos+SdiRv2JJRu^{5Ta61%__Nk+vwUo&BNLgtr@$_SRdg3U1p-uT&^Z~grI0INB(Lf^=*TY)beOiv7 zDRb5JSj`1Eo3m`wap0|ul1~~X%n!gsDc+H#HQ(4RWQ-rn9+DM4$H01GzQg||Lhf8R z*RbOX5lSEWjQ+!Y+TRt@NZPPl_oA-?oCTc}B6nwCG4pO)wnxA+n3`(p8Xn_LIhxOv z51o0cLL1V$)XMMSc`91XP6p^=#H0;6X6nb>ccTN4jmWP^S{`W!WGoa8 z1~zzVW+PRues5=vT|d24tDQbxqZqmTJJOu@X4D-%+m*j1ysYOAHTm zkR=(RN3Dg_m|#7$uGOHzwoc(GSz))s^RU5V{SI2PhHm#46LqA+zj%GtdoC)Qm5!N-Ke9X1J6t7=l3T$YqGv6$*Jj`qEZuwJ`dm?U!kr<2=P;IzA-}aEwDI+eKBIz;?A6^2fkw7>`P;0bUq`J~@bW%_ zbaoEx1BT5N;)FZolO{;z`}~NinK~?_7m|2J&4JU{n!EHUzbQ8QW7EQnA&+Vf=4Z_X zFHNh3wYl(ZYkB1 ziEpIZ-OqV>@2_N-cVl7tgfCDQ&b%@6Likekz7{Tssgcg7px3A``EVnp@TM`^c4+$o zKlMTOH?&7%>y!<7FuoWwmb9|CH_X{a6dS!Frk{R~=%zSz^^{Y1h1&=ql!Oa9?5&Dg zYHN{k{5(wyTQK&`5A&8#zo?`Tz8;Uwq21>k4b9R^n3(rG$PiHd_hs##N06M3f7-xR zJ_{S~-`mG1`{~I9PG26YPVZzC1-a~-*)O35`=Zv{K?b1M#Rqa@w%#x7PjR@&qpE91 zXn@WD0C2X+V38H?p z!HCH=lI$Z(V~y-wC5$r(DK!N z)|>qa^f#j2_*BYQ$&5li7{(awD<2+c_q1TI-ewX^8 zW#zW@s)a;wz26mwYIKj^Z3~hR!kP@ScMlsg+1aW8J@pc-8g`S6f@g@zmkh$fWD6yt zvxQXCGjwBRrQ9h93|Nllq1H10q*s7`V0yk=174&HQ^z5tWM|GvYJAK{twC;m>_D3d zz_-7>FdsyfnwZ0JHMfUe)z_guBG8?((KZxAb%L6~=j2$3*8&B6#9Q0nco~%VPMd-# zz}(JTjWz(flPuuT3d%VO&pX^^Oqk1A4Na|hV1vd1WC={~#!)NfW{;Cs=(+Cv_HDB!dCjHlPOk{9{=_ zOV0lMP)i5w}QW)EbWtt!^~!z@zr*M&!yGmh!#caH*4V5>`TKqT7x^KUYC`pNcVeDFm^W+hg@foPXIh=e}nxArjhA2Y%u4EqQ#? z-oONM;N*IEam=sI57o4X{b#@S z`RpSSjW_bkqFLj-@U}@Dhy&ebc>CJa{6ZiBpimz&pgy_(8tWuC6_7C*#PgyFH7q~N zGP^zF!S;537r5xepElnt;(6qI3W{V&{oI-Y-b$KejpVRjLXdJ>`~Bav*#d?r{R@&W zI`%_rFn{3)U}YkOZjKmDHxk@|V$H(=A>k38la?`j> z7#RS?!!98~Yt`zxVf1S?YE#v!b(uh4W#v2Z@ZeKI;b@5HMv7CK=muYn1V@sbGlbUOfOHGh&6r2b5fu^J1| z6@4-y&|abRfQ7xvPohJ$6KfidvPmn#G3Tgz@n(*<7)E`XKU?W1&`Els)|SpOu~L~3 z;x}sh&0R_TM*{*bVyU;Z&zB`qunl0Ro7x{*l|tfzhVdj8fBD@ z1C~(~epU-mfr(T&s>f00B(-QjU4odxjFjY12lfL5O`7kocC^jjX!KZo`zvkI727%E z)-mb;+LX56yrPL%X=d)mJ>ZS~P0HNLnMYms#6-%ue_8s!$JBS@mN=Opvt7#h~kRDgY|% zmdqPbPrkRbYhMchS6UV4}&D8RYV)tBQqH8lQK$DDVAwiE%q^gYzZBY>l{hM+%A(g`CS-r_eF9PaYra z5noA{6pnksm=aqSwX)O+L;&q{w%Rop9COpTQ3d!`m@*DX5=3&wzN1Z<2)q|{i z5HkjVSYRq1xr=DGUXx%?APdIb`Mye0KBiDxpHj{P#r%d-FaXc7#tGy z6>fPyfKll@kfNAPGSvfJHaqNbTGyHZNnY@yYqIBMQy&;rvmHOw4~l3=GbM(Jd9gNY zv$vXdEqdF5>4)Xq!Lcs-LHb~4&D^h)Ku7YtYt22UlfR!=&JulDW(vf^w-ru)_EfXm zp!ls-Cx5vTV0ETe=dfspX1bI04)AG+)xCEZ88PrkCYlPLVas}8Qh(aMJCWj$z_y#z z%F@TEg$Qqsk@>^Hd>_AYDgMUHK!8L1()BRQBkRh!9J`0?Y|;XZ{oC+?i$^R%IEl=+ zl|Bg$wA;sW!kT{kK9$_qB>PC(i!TZOGToR2p zj<@Z+42PSME2-YIn|aw02LX9#u7Gkf5Rc%w7wyhO$naofV1wKm|9}M2a5Ut-_6g$Z zv988we(LEnQ-|uLXi9)xQnw4pc6|Dum6i!cSJ3F@4F$^I2PxX}8p2xwx13QP>>WBz zRYoL9dgmPlFVFSWtd6}QH4MK0z_{Lb$g2J|YHA7X7a;lc+s=J=Ucga?!Pel>m5kQP ztW7%x`_tgjFDvm90T-t8s0rXhDOOL*39=iC+0m&D0a_tdT#i`^GQWtAL_YouV(HU^ zv8}A3)<2~6K9^&Y!m`@-Q3kU$>h$&1afHk1eIr#HrRwbKYfYt%P|W7f*KbEY_W;}Y zXvxlfhGO_F0wcOM!sj?EE>FwFNp8+2uQ1UIyBE`}H*~#S>>BQ#@Aq3?n=wy1LA}s- z=j+QuaW&sC-?sKJ2y@Z0Kn-AY*JVr}DvPQ$db-l&-m- zQx2+(tCBj%Wo_=au_3RI*fNO5^oD;Abw z9Ml?fr(T{(wOQ}bVe6K^ZtWTg2N^`9WEnLQRknl(!|T2@)9SQtl?MVM0OwqU zCawp=`t#L*2h-^5vnu+susR^x~fEO8L75L}3QaqU3T@ zRr(7~ziJTZLY&NoXhMoWF#Rh%2iEYW`_sZXut)?tYJW4);^-M3Z>ak^S@igwi;(A* zfioxC=tdPubA!PbC(z@DR7!bhNJ4@Hu<0)4Gl|a;r5=Hj_R%VTwo`lyxzD^-Tw>`_ zUI;M^_L6@W0yE(r6Ur`j6G2EmkJ>Q70aXj_$?viSfGzn1=ueLM97>8jlR!%d#6t%% zlJ*2+I|t@|Id?YG&qyYJx_n}8z=G|?wfrOJ-rN4>-nhQm_3gozdFEm|vIl*s`30}@ zO9SHi!;VINlbfgfVYfFb700wyZ10%IG$i zJ>3-#u(qxQu}dRaOB@z@6U=-2g(osX>B!hbZR;zIYH-bnr2nt=tLxoi%gF&;p0B#7 zuE;A~C2!<4-Wr;NXqQ!bZ_wvdRXkqIPQJ&>GjnqVQ9!NU5-Uvx%JR1KnwG##gOqNu zQ`4nc8N*W}tb&4g@bo)6w$+J*a;$lSmVmQ-@N@g4EzIl{u@j1f4Tju(U|KB(Lb-Ek9JJP+upKJh)G9lgt?_3a10_S9m z>G14EL6`}lKhxX030N@PnK3Y}ok_*lF=OH=0TzW5Y^VJo<#>2v) literal 0 HcmV?d00001 diff --git a/crates/red_knot/docs/tracing.md b/crates/red_knot/docs/tracing.md new file mode 100644 index 0000000000..98b4665e28 --- /dev/null +++ b/crates/red_knot/docs/tracing.md @@ -0,0 +1,103 @@ +# Tracing + +Traces are a useful tool to narrow down the location of a bug or, at least, to understand why the compiler is doing a particular thing. +Note, tracing messages with severity `debug` or greater are user-facing. They should be phrased accordingly. +Tracing spans are only shown when using `-vvv`. + +## Verbosity levels + +The CLI supports different verbosity levels. + +- default: Only show errors and warnings. +- `-v` activates `info!`: Show generally useful information such as paths of configuration files, detected platform, etc., but it's not a lot of messages, it's something you'll activate in CI by default. cargo build e.g. shows you which packages are fresh. +- `-vv` activates `debug!` and timestamps: This should be enough information to get to the bottom of bug reports. When you're processing many packages or files, you'll get pages and pages of output, but each line is link to a specific action or state change. +- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you almost certainly want to use `RED_KNOT_LOG` to filter it down to the area your investigating. + +## `RED_KNOT_LOG` + +By default, the CLI shows messages from the `ruff` and `red_knot` crates. Tracing messages from other crates are not shown. +The `RED_KNOT_LOG` environment variable allows you to customize which messages are shown by specifying one +or more [filter directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives). + +### Examples + +#### Show all debug messages + +Shows debug messages from all crates. + +```bash +RED_KNOT_LOG=debug +``` + +#### Show salsa query execution messages + +Show the salsa `execute: my_query` messages in addition to all red knot messages. + +```bash +RED_KNOT_LOG=ruff=trace,red_knot=trace,salsa=info +``` + +#### Show typing traces + +Only show traces for the `red_knot_python_semantic::types` module. + +```bash +RED_KNOT_LOG="red_knot_python_semantic::types" +``` + +Note: Ensure that you use `-vvv` to see tracing spans. + +#### Show messages for a single file + +Shows all messages that are inside of a span for a specific file. + +```bash +RED_KNOT_LOG=red_knot[{file=/home/micha/astral/test/x.py}]=trace +``` + +**Note**: Tracing still shows all spans because tracing can't know at the time of entering the span +whether one if its children has the file `x.py`. + +**Note**: Salsa currently logs the entire memoized values. In our case, the source text and parsed AST. +This very quickly leads to extremely long outputs. + +## Tracing and Salsa + +Be mindful about using `tracing` in Salsa queries, especially when using `warn` or `error` because it isn't guaranteed +that the query will execute after restoring from a persistent cache. In which case the user won't see the message. + +For example, don't use `tracing` to show the user a message when generating a lint violation failed +because the message would only be shown when linting the file the first time, but not on subsequent analysis +runs or when restoring from a persistent cache. This can be confusing for users because they +don't understand why a specific lint violation isn't raised. Instead, change your +query to return the failure as part of the query's result or use a Salsa accumulator. + +## Release builds + +`trace!` events are removed in release builds. + +## Profiling + +Red Knot generates a folded stack trace to the current directory named `tracing.folded` when setting the environment variable `RED_KNOT_LOG_PROFILE` to `1` or `true`. + +```bash +RED_KNOT_LOG_PROFILE=1 red_knot -- --current-directory=../test -vvv +``` + +You can convert the textual representation into a visual one using `inferno`. + +```shell +cargo install inferno +``` + +```shell +# flamegraph +cat tracing.folded | inferno-flamegraph > tracing-flamegraph.svg + +# flamechart +cat tracing.folded | inferno-flamegraph --flamechart > tracing-flamechart.svg +``` + +![Example flamegraph](./tracing-flamegraph.png) + +See [`tracing-flame`](https://crates.io/crates/tracing-flame) for more details. diff --git a/crates/red_knot/src/logging.rs b/crates/red_knot/src/logging.rs new file mode 100644 index 0000000000..8ceff9472e --- /dev/null +++ b/crates/red_knot/src/logging.rs @@ -0,0 +1,254 @@ +//! Sets up logging for Red Knot + +use anyhow::Context; +use colored::Colorize; +use std::fmt; +use std::fs::File; +use std::io::BufWriter; +use tracing::log::LevelFilter; +use tracing::{Event, Subscriber}; +use tracing_subscriber::fmt::format::Writer; +use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields}; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::EnvFilter; + +/// Logging flags to `#[command(flatten)]` into your CLI +#[derive(clap::Args, Debug, Clone, Default)] +#[command(about = None, long_about = None)] +pub(crate) struct Verbosity { + #[arg( + long, + short = 'v', + help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)", + action = clap::ArgAction::Count, + global = true, + )] + verbose: u8, +} + +impl Verbosity { + /// Returns the verbosity level based on the number of `-v` flags. + /// + /// Returns `None` if the user did not specify any verbosity flags. + pub(crate) fn level(&self) -> VerbosityLevel { + match self.verbose { + 0 => VerbosityLevel::Default, + 1 => VerbosityLevel::Verbose, + 2 => VerbosityLevel::ExtraVerbose, + _ => VerbosityLevel::Trace, + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub(crate) enum VerbosityLevel { + /// Default output level. Only shows Ruff and Red Knot events up to the [`WARN`](tracing::Level::WARN). + Default, + + /// Enables verbose output. Emits Ruff and Red Knot events up to the [`INFO`](tracing::Level::INFO). + /// Corresponds to `-v`. + Verbose, + + /// Enables a more verbose tracing format and emits Ruff and Red Knot events up to [`DEBUG`](tracing::Level::DEBUG). + /// Corresponds to `-vv` + ExtraVerbose, + + /// Enables all tracing events and uses a tree-like output format. Corresponds to `-vvv`. + Trace, +} + +impl VerbosityLevel { + const fn level_filter(self) -> LevelFilter { + match self { + VerbosityLevel::Default => LevelFilter::Warn, + VerbosityLevel::Verbose => LevelFilter::Info, + VerbosityLevel::ExtraVerbose => LevelFilter::Debug, + VerbosityLevel::Trace => LevelFilter::Trace, + } + } + + pub(crate) const fn is_trace(self) -> bool { + matches!(self, VerbosityLevel::Trace) + } + + pub(crate) const fn is_extra_verbose(self) -> bool { + matches!(self, VerbosityLevel::ExtraVerbose) + } +} + +pub(crate) fn setup_tracing(level: VerbosityLevel) -> anyhow::Result { + use tracing_subscriber::prelude::*; + + // The `RED_KNOT_LOG` environment variable overrides the default log level. + let filter = if let Ok(log_env_variable) = std::env::var("RED_KNOT_LOG") { + EnvFilter::builder() + .parse(log_env_variable) + .context("Failed to parse directives specified in RED_KNOT_LOG environment variable.")? + } else { + match level { + VerbosityLevel::Default => { + // Show warning traces + EnvFilter::default().add_directive(tracing::level_filters::LevelFilter::WARN.into()) + } + level => { + let level_filter = level.level_filter(); + + // Show info|debug|trace events, but allow `RED_KNOT_LOG` to override + let filter = EnvFilter::default().add_directive( + format!("red_knot={level_filter}") + .parse() + .expect("Hardcoded directive to be valid"), + ); + + filter.add_directive( + format!("ruff={level_filter}") + .parse() + .expect("Hardcoded directive to be valid"), + ) + } + } + }; + + let (profiling_layer, guard) = setup_profile(); + + let registry = tracing_subscriber::registry() + .with(filter) + .with(profiling_layer); + + if level.is_trace() { + let subscriber = registry.with( + tracing_tree::HierarchicalLayer::default() + .with_indent_lines(true) + .with_indent_amount(2) + .with_bracketed_fields(true) + .with_thread_ids(true) + .with_targets(true) + .with_writer(std::io::stderr) + .with_timer(tracing_tree::time::Uptime::default()), + ); + + subscriber.init(); + } else { + let subscriber = registry.with( + tracing_subscriber::fmt::layer() + .event_format(RedKnotFormat { + display_level: true, + display_timestamp: level.is_extra_verbose(), + show_spans: false, + }) + .with_writer(std::io::stderr), + ); + + subscriber.init(); + } + + Ok(TracingGuard { + _flame_guard: guard, + }) +} + +#[allow(clippy::type_complexity)] +fn setup_profile() -> ( + Option>>, + Option>>, +) +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + if let Ok("1" | "true") = std::env::var("RED_KNOT_LOG_PROFILE").as_deref() { + let (layer, guard) = tracing_flame::FlameLayer::with_file("tracing.folded") + .expect("Flame layer to be created"); + (Some(layer), Some(guard)) + } else { + (None, None) + } +} + +pub(crate) struct TracingGuard { + _flame_guard: Option>>, +} + +struct RedKnotFormat { + display_timestamp: bool, + display_level: bool, + show_spans: bool, +} + +/// See +impl FormatEvent for RedKnotFormat +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &FmtContext<'_, S, N>, + mut writer: Writer<'_>, + event: &Event<'_>, + ) -> fmt::Result { + let meta = event.metadata(); + let ansi = writer.has_ansi_escapes(); + + if self.display_timestamp { + let timestamp = chrono::Local::now() + .format("%Y-%m-%d %H:%M:%S.%f") + .to_string(); + if ansi { + write!(writer, "{} ", timestamp.dimmed())?; + } else { + write!( + writer, + "{} ", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S.%f") + )?; + } + } + + if self.display_level { + let level = meta.level(); + // Same colors as tracing + if ansi { + let formatted_level = level.to_string(); + match *level { + tracing::Level::TRACE => { + write!(writer, "{} ", formatted_level.purple().bold())?; + } + tracing::Level::DEBUG => write!(writer, "{} ", formatted_level.blue().bold())?, + tracing::Level::INFO => write!(writer, "{} ", formatted_level.green().bold())?, + tracing::Level::WARN => write!(writer, "{} ", formatted_level.yellow().bold())?, + tracing::Level::ERROR => write!(writer, "{} ", level.to_string().red().bold())?, + } + } else { + write!(writer, "{level} ")?; + } + } + + if self.show_spans { + let span = event.parent(); + let mut seen = false; + + let span = span + .and_then(|id| ctx.span(id)) + .or_else(|| ctx.lookup_current()); + + let scope = span.into_iter().flat_map(|span| span.scope().from_root()); + + for span in scope { + seen = true; + if ansi { + write!(writer, "{}:", span.metadata().name().bold())?; + } else { + write!(writer, "{}:", span.metadata().name())?; + } + } + + if seen { + writer.write_char(' ')?; + } + } + + ctx.field_format().format_fields(writer.by_ref(), event)?; + + writeln!(writer) + } +} diff --git a/crates/red_knot/src/main.rs b/crates/red_knot/src/main.rs index 1a6a555be2..179c866481 100644 --- a/crates/red_knot/src/main.rs +++ b/crates/red_knot/src/main.rs @@ -4,12 +4,6 @@ use std::sync::Mutex; use clap::Parser; use crossbeam::channel as crossbeam_channel; use red_knot_workspace::site_packages::site_packages_dirs_of_venv; -use tracing::subscriber::Interest; -use tracing::{Level, Metadata}; -use tracing_subscriber::filter::LevelFilter; -use tracing_subscriber::layer::{Context, Filter, SubscriberExt}; -use tracing_subscriber::{Layer, Registry}; -use tracing_tree::time::Uptime; use red_knot_workspace::db::RootDatabase; use red_knot_workspace::watch; @@ -18,8 +12,10 @@ use red_knot_workspace::workspace::WorkspaceMetadata; use ruff_db::program::{ProgramSettings, SearchPathSettings}; use ruff_db::system::{OsSystem, System, SystemPathBuf}; use target_version::TargetVersion; -use verbosity::{Verbosity, VerbosityLevel}; +use crate::logging::{setup_tracing, Verbosity}; + +mod logging; mod target_version; mod verbosity; @@ -106,7 +102,7 @@ pub fn main() -> anyhow::Result<()> { } = Args::parse_from(std::env::args().collect::>()); let verbosity = verbosity.level(); - countme::enable(verbosity == Some(VerbosityLevel::Trace)); + countme::enable(verbosity.is_trace()); if matches!(command, Some(Command::Server)) { let four = NonZeroUsize::new(4).unwrap(); @@ -119,7 +115,7 @@ pub fn main() -> anyhow::Result<()> { return red_knot_server::Server::new(worker_threads)?.run(); } - setup_tracing(verbosity); + let _guard = setup_tracing(verbosity)?; let cwd = if let Some(cwd) = current_directory { let canonicalized = cwd.as_utf8_path().canonicalize_utf8().unwrap(); @@ -159,7 +155,7 @@ pub fn main() -> anyhow::Result<()> { // cache and load the cache if it exists. let mut db = RootDatabase::new(workspace_metadata, program_settings, system); - let (main_loop, main_loop_cancellation_token) = MainLoop::new(verbosity); + let (main_loop, main_loop_cancellation_token) = MainLoop::new(); // Listen to Ctrl+C and abort the watch mode. let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token)); @@ -177,6 +173,8 @@ pub fn main() -> anyhow::Result<()> { main_loop.run(&mut db); }; + tracing::trace!("Counts for entire CLI run :\n{}", countme::get_all()); + std::mem::forget(db); Ok(()) @@ -191,12 +189,10 @@ struct MainLoop { /// The file system watcher, if running in watch mode. watcher: Option, - - verbosity: Option, } impl MainLoop { - fn new(verbosity: Option) -> (Self, MainLoopCancellationToken) { + fn new() -> (Self, MainLoopCancellationToken) { let (sender, receiver) = crossbeam_channel::bounded(10); ( @@ -204,32 +200,41 @@ impl MainLoop { sender: sender.clone(), receiver, watcher: None, - verbosity, }, MainLoopCancellationToken { sender }, ) } fn watch(mut self, db: &mut RootDatabase) -> anyhow::Result<()> { + tracing::debug!("Starting watch mode"); let sender = self.sender.clone(); let watcher = watch::directory_watcher(move |event| { sender.send(MainLoopMessage::ApplyChanges(event)).unwrap(); })?; self.watcher = Some(WorkspaceWatcher::new(watcher, db)); + self.run(db); + Ok(()) } - #[allow(clippy::print_stderr)] fn run(mut self, db: &mut RootDatabase) { - // Schedule the first check. self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); + + self.main_loop(db); + + tracing::debug!("Exiting main loop"); + } + + #[allow(clippy::print_stderr)] + fn main_loop(&mut self, db: &mut RootDatabase) { + // Schedule the first check. + tracing::debug!("Starting main loop"); + let mut revision = 0usize; while let Ok(message) = self.receiver.recv() { - tracing::trace!("Main Loop: Tick"); - match message { MainLoopMessage::CheckWorkspace => { let db = db.snapshot(); @@ -253,15 +258,15 @@ impl MainLoop { } => { if check_revision == revision { eprintln!("{}", result.join("\n")); - - if self.verbosity == Some(VerbosityLevel::Trace) { - eprintln!("{}", countme::get_all()); - } + } else { + tracing::debug!("Discarding check result for outdated revision: current: {revision}, result revision: {check_revision}"); } if self.watcher.is_none() { - return self.exit(); + return; } + + tracing::trace!("Counts after last check:\n{}", countme::get_all()); } MainLoopMessage::ApplyChanges(changes) => { @@ -274,19 +279,11 @@ impl MainLoop { self.sender.send(MainLoopMessage::CheckWorkspace).unwrap(); } MainLoopMessage::Exit => { - return self.exit(); + return; } } - } - self.exit(); - } - - #[allow(clippy::print_stderr, clippy::unused_self)] - fn exit(self) { - if self.verbosity == Some(VerbosityLevel::Trace) { - eprintln!("Exit"); - eprintln!("{}", countme::get_all()); + tracing::debug!("Waiting for next main loop message."); } } } @@ -313,63 +310,3 @@ enum MainLoopMessage { ApplyChanges(Vec), Exit, } - -fn setup_tracing(verbosity: Option) { - let trace_level = match verbosity { - None => Level::WARN, - Some(VerbosityLevel::Info) => Level::INFO, - Some(VerbosityLevel::Debug) => Level::DEBUG, - Some(VerbosityLevel::Trace) => Level::TRACE, - }; - - let subscriber = Registry::default().with( - tracing_tree::HierarchicalLayer::default() - .with_indent_lines(true) - .with_indent_amount(2) - .with_bracketed_fields(true) - .with_thread_ids(true) - .with_targets(true) - .with_writer(|| Box::new(std::io::stderr())) - .with_timer(Uptime::default()) - .with_filter(LoggingFilter { trace_level }), - ); - - tracing::subscriber::set_global_default(subscriber).unwrap(); -} - -struct LoggingFilter { - trace_level: Level, -} - -impl LoggingFilter { - fn is_enabled(&self, meta: &Metadata<'_>) -> bool { - let filter = if meta.target().starts_with("red_knot") || meta.target().starts_with("ruff") { - self.trace_level - } else if meta.target().starts_with("salsa") && self.trace_level <= Level::INFO { - // Salsa emits very verbose query traces with level info. Let's not show these to the user. - Level::WARN - } else { - Level::INFO - }; - - meta.level() <= &filter - } -} - -impl Filter for LoggingFilter { - fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool { - self.is_enabled(meta) - } - - fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest { - if self.is_enabled(meta) { - Interest::always() - } else { - Interest::never() - } - } - - fn max_level_hint(&self) -> Option { - Some(LevelFilter::from_level(self.trace_level)) - } -} diff --git a/crates/red_knot/src/verbosity.rs b/crates/red_knot/src/verbosity.rs index 692553bcd9..8b13789179 100644 --- a/crates/red_knot/src/verbosity.rs +++ b/crates/red_knot/src/verbosity.rs @@ -1,34 +1 @@ -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub(crate) enum VerbosityLevel { - Info, - Debug, - Trace, -} -/// Logging flags to `#[command(flatten)]` into your CLI -#[derive(clap::Args, Debug, Clone, Default)] -#[command(about = None, long_about = None)] -pub(crate) struct Verbosity { - #[arg( - long, - short = 'v', - help = "Use verbose output (or `-vv` and `-vvv` for more verbose output)", - action = clap::ArgAction::Count, - global = true, - )] - verbose: u8, -} - -impl Verbosity { - /// Returns the verbosity level based on the number of `-v` flags. - /// - /// Returns `None` if the user did not specify any verbosity flags. - pub(crate) fn level(&self) -> Option { - match self.verbose { - 0 => None, - 1 => Some(VerbosityLevel::Info), - 2 => Some(VerbosityLevel::Debug), - _ => Some(VerbosityLevel::Trace), - } - } -} diff --git a/crates/red_knot_module_resolver/src/resolver.rs b/crates/red_knot_module_resolver/src/resolver.rs index 14a7c826db..5833da5c5a 100644 --- a/crates/red_knot_module_resolver/src/resolver.rs +++ b/crates/red_knot_module_resolver/src/resolver.rs @@ -32,10 +32,18 @@ pub(crate) fn resolve_module_query<'db>( let name = module_name.name(db); let _span = tracing::trace_span!("resolve_module", %name).entered(); - let (search_path, module_file, kind) = resolve_name(db, name)?; + let Some((search_path, module_file, kind)) = resolve_name(db, name) else { + tracing::debug!("Module '{name}' not found in the search paths."); + return None; + }; let module = Module::new(name.clone(), kind, search_path, module_file); + tracing::debug!( + "Resolved module '{name}' to '{path}'.", + path = module_file.path(db) + ); + Some(module) } diff --git a/crates/red_knot_python_semantic/src/semantic_index.rs b/crates/red_knot_python_semantic/src/semantic_index.rs index 45d24a599d..54d0ba3b33 100644 --- a/crates/red_knot_python_semantic/src/semantic_index.rs +++ b/crates/red_knot_python_semantic/src/semantic_index.rs @@ -34,7 +34,7 @@ type SymbolMap = hashbrown::HashMap; /// Prefer using [`symbol_table`] when working with symbols from a single scope. #[salsa::tracked(return_ref, no_eq)] pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { - let _span = tracing::trace_span!("semantic_index", file=?file.path(db)).entered(); + let _span = tracing::trace_span!("semantic_index", file = %file.path(db)).entered(); let parsed = parsed_module(db.upcast(), file); @@ -50,7 +50,7 @@ pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc { let file = scope.file(db); let _span = - tracing::trace_span!("symbol_table", scope=?scope.as_id(), file=?file.path(db)).entered(); + tracing::trace_span!("symbol_table", scope=?scope.as_id(), file=%file.path(db)).entered(); let index = semantic_index(db, file); index.symbol_table(scope.file_scope_id(db)) @@ -65,7 +65,7 @@ pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc> { let file = scope.file(db); let _span = - tracing::trace_span!("use_def_map", scope=?scope.as_id(), file=?file.path(db)).entered(); + tracing::trace_span!("use_def_map", scope=?scope.as_id(), file=%file.path(db)).entered(); let index = semantic_index(db, file); index.use_def_map(scope.file_scope_id(db)) @@ -74,7 +74,7 @@ pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc ScopeId<'_> { - let _span = tracing::trace_span!("global_scope", file=?file.path(db)).entered(); + let _span = tracing::trace_span!("global_scope", file = %file.path(db)).entered(); FileScopeId::global().to_scope_id(db, file) } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 769a000655..a0853200c2 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -50,7 +50,7 @@ use crate::Db; pub(crate) fn infer_scope_types<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> TypeInference<'db> { let file = scope.file(db); let _span = - tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), file=?file.path(db)) + tracing::trace_span!("infer_scope_types", scope=?scope.as_id(), file=%file.path(db)) .entered(); // Using the index here is fine because the code below depends on the AST anyway. @@ -83,7 +83,7 @@ pub(crate) fn infer_definition_types<'db>( let _span = tracing::trace_span!( "infer_definition_types", definition = ?definition.as_id(), - file = ?file.path(db) + file = %file.path(db) ) .entered(); @@ -104,7 +104,7 @@ pub(crate) fn infer_expression_types<'db>( ) -> TypeInference<'db> { let file = expression.file(db); let _span = - tracing::trace_span!("infer_expression_types", expression=?expression.as_id(), file=?file.path(db)) + tracing::trace_span!("infer_expression_types", expression=?expression.as_id(), file=%file.path(db)) .entered(); let index = semantic_index(db, file); diff --git a/crates/red_knot_workspace/src/lint.rs b/crates/red_knot_workspace/src/lint.rs index 812f2c0612..c2b57440d1 100644 --- a/crates/red_knot_workspace/src/lint.rs +++ b/crates/red_knot_workspace/src/lint.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::ops::Deref; use std::time::Duration; -use tracing::trace_span; +use tracing::debug_span; use red_knot_module_resolver::ModuleName; use red_knot_python_semantic::types::Type; @@ -76,7 +76,7 @@ fn lint_lines(source: &str, diagnostics: &mut Vec) { #[allow(unreachable_pub)] #[salsa::tracked(return_ref)] pub fn lint_semantic(db: &dyn Db, file_id: File) -> Diagnostics { - let _span = trace_span!("lint_semantic", file=?file_id.path(db)).entered(); + let _span = debug_span!("lint_semantic", file=%file_id.path(db)).entered(); let source = source_text(db.upcast(), file_id); let parsed = parsed_module(db.upcast(), file_id); diff --git a/crates/red_knot_workspace/src/site_packages.rs b/crates/red_knot_workspace/src/site_packages.rs index 0e6585825b..c5d26f20ed 100644 --- a/crates/red_knot_workspace/src/site_packages.rs +++ b/crates/red_knot_workspace/src/site_packages.rs @@ -94,6 +94,10 @@ fn site_packages_dir_from_sys_prefix( let site_packages_candidate = path.join("site-packages"); if system.is_directory(&site_packages_candidate) { + tracing::debug!( + "Resoled site-packages directory: {}", + site_packages_candidate + ); return Ok(site_packages_candidate); } } diff --git a/crates/red_knot_workspace/src/watch/watcher.rs b/crates/red_knot_workspace/src/watch/watcher.rs index 61205530a0..ff3e010097 100644 --- a/crates/red_knot_workspace/src/watch/watcher.rs +++ b/crates/red_knot_workspace/src/watch/watcher.rs @@ -109,6 +109,8 @@ struct WatcherInner { impl Watcher { /// Sets up file watching for `path`. pub fn watch(&mut self, path: &SystemPath) -> notify::Result<()> { + tracing::debug!("Watching path: {path}."); + self.inner_mut() .watcher .watch(path.as_std_path(), RecursiveMode::Recursive) @@ -116,6 +118,8 @@ impl Watcher { /// Stops file watching for `path`. pub fn unwatch(&mut self, path: &SystemPath) -> notify::Result<()> { + tracing::debug!("Unwatching path: {path}."); + self.inner_mut().watcher.unwatch(path.as_std_path()) } @@ -125,6 +129,7 @@ impl Watcher { /// /// The call blocks until the watcher has stopped. pub fn stop(mut self) { + tracing::debug!("Stop file watcher"); self.set_stop(); } @@ -173,8 +178,8 @@ struct Debouncer { } impl Debouncer { - #[tracing::instrument(level = "trace", skip(self))] fn add_result(&mut self, result: notify::Result) { + tracing::trace!("Handling file watcher event: {result:?}."); match result { Ok(event) => self.add_event(event), Err(error) => self.add_error(error), diff --git a/crates/red_knot_workspace/src/workspace.rs b/crates/red_knot_workspace/src/workspace.rs index 584eae83da..f70f535c4a 100644 --- a/crates/red_knot_workspace/src/workspace.rs +++ b/crates/red_knot_workspace/src/workspace.rs @@ -120,8 +120,8 @@ impl Workspace { self.package_tree(db).values().copied() } - #[tracing::instrument(skip_all)] pub fn reload(self, db: &mut dyn Db, metadata: WorkspaceMetadata) { + tracing::debug!("Reloading workspace"); assert_eq!(self.root(db), metadata.root()); let mut old_packages = self.package_tree(db).clone(); @@ -145,7 +145,6 @@ impl Workspace { .to(new_packages); } - #[tracing::instrument(level = "debug", skip_all)] pub fn update_package(self, db: &mut dyn Db, metadata: PackageMetadata) -> anyhow::Result<()> { let path = metadata.root().to_path_buf(); @@ -176,6 +175,8 @@ impl Workspace { /// Checks all open files in the workspace and its dependencies. #[tracing::instrument(level = "debug", skip_all)] pub fn check(self, db: &dyn Db) -> Vec { + tracing::debug!("Checking workspace"); + let mut result = Vec::new(); if let Some(open_files) = self.open_files(db) { @@ -194,16 +195,18 @@ impl Workspace { /// Opens a file in the workspace. /// /// This changes the behavior of `check` to only check the open files rather than all files in the workspace. - #[tracing::instrument(level = "debug", skip(self, db))] pub fn open_file(self, db: &mut dyn Db, file: File) { + tracing::debug!("Opening file {}", file.path(db)); + let mut open_files = self.take_open_files(db); open_files.insert(file); self.set_open_files(db, open_files); } /// Closes a file in the workspace. - #[tracing::instrument(level = "debug", skip(self, db))] pub fn close_file(self, db: &mut dyn Db, file: File) -> bool { + tracing::debug!("Closing file {}", file.path(db)); + let mut open_files = self.take_open_files(db); let removed = open_files.remove(&file); @@ -224,6 +227,8 @@ impl Workspace { /// This changes the behavior of `check` to only check the open files rather than all files in the workspace. #[tracing::instrument(level = "debug", skip(self, db))] pub fn set_open_files(self, db: &mut dyn Db, open_files: FxHashSet) { + tracing::debug!("Set open workspace files (count: {})", open_files.len()); + self.set_open_fileset(db).to(Some(Arc::new(open_files))); } @@ -231,6 +236,8 @@ impl Workspace { /// /// This changes the behavior of `check` to check all files in the workspace instead of just the open files. pub fn take_open_files(self, db: &mut dyn Db) -> FxHashSet { + tracing::debug!("Take open workspace files"); + // Salsa will cancel any pending queries and remove its own reference to `open_files` // so that the reference counter to `open_files` now drops to 1. let open_files = self.set_open_fileset(db).to(None); @@ -256,6 +263,12 @@ impl Package { #[tracing::instrument(level = "debug", skip(db))] pub fn remove_file(self, db: &mut dyn Db, file: File) { + tracing::debug!( + "Remove file {} from package {}", + file.path(db), + self.name(db) + ); + let Some(mut index) = PackageFiles::indexed_mut(db, self) else { return; }; @@ -263,8 +276,9 @@ impl Package { index.remove(file); } - #[tracing::instrument(level = "debug", skip(db))] pub fn add_file(self, db: &mut dyn Db, file: File) { + tracing::debug!("Add file {} to package {}", file.path(db), self.name(db)); + let Some(mut index) = PackageFiles::indexed_mut(db, self) else { return; }; @@ -274,6 +288,8 @@ impl Package { #[tracing::instrument(level = "debug", skip(db))] pub(crate) fn check(self, db: &dyn Db) -> Vec { + tracing::debug!("Checking package {}", self.root(db)); + let mut result = Vec::new(); for file in &self.files(db).read() { let diagnostics = check_file(db, file); @@ -286,10 +302,12 @@ impl Package { /// Returns the files belonging to this package. #[salsa::tracked] pub fn files(self, db: &dyn Db) -> IndexedFiles { + let _entered = tracing::debug_span!("files").entered(); let files = self.file_set(db); let indexed = match files.get() { Index::Lazy(vacant) => { + tracing::debug!("Indexing files for package {}", self.name(db)); let files = discover_package_files(db, self.root(db)); vacant.set(files) } @@ -317,8 +335,9 @@ impl Package { } } - #[tracing::instrument(level = "debug", skip(db))] pub fn reload_files(self, db: &mut dyn Db) { + tracing::debug!("Reload files for package {}", self.name(db)); + if !self.file_set(db).is_lazy() { // Force a re-index of the files in the next revision. self.set_file_set(db).to(PackageFiles::lazy()); @@ -327,6 +346,10 @@ impl Package { } pub(super) fn check_file(db: &dyn Db, file: File) -> Diagnostics { + let path = file.path(db); + let _span = tracing::debug_span!("check_file", file=%path).entered(); + tracing::debug!("Checking file {path}"); + let mut diagnostics = Vec::new(); diagnostics.extend_from_slice(lint_syntax(db, file)); diagnostics.extend_from_slice(lint_semantic(db, file)); diff --git a/crates/ruff_db/src/files.rs b/crates/ruff_db/src/files.rs index 74940fc463..cf17740afc 100644 --- a/crates/ruff_db/src/files.rs +++ b/crates/ruff_db/src/files.rs @@ -77,7 +77,6 @@ impl Files { /// /// The operation always succeeds even if the path doesn't exist on disk, isn't accessible or if the path points to a directory. /// In these cases, a file with status [`FileStatus::NotFound`] is returned. - #[tracing::instrument(level = "trace", skip(self, db))] fn system(&self, db: &dyn Db, path: &SystemPath) -> File { let absolute = SystemPath::absolute(path, db.system().current_directory()); @@ -86,6 +85,8 @@ impl Files { .system_by_path .entry(absolute.clone()) .or_insert_with(|| { + tracing::trace!("Adding file {path}"); + let metadata = db.system().path_metadata(path); let durability = self .root(db, path) @@ -118,7 +119,6 @@ impl Files { /// Looks up a vendored file by its path. Returns `Some` if a vendored file for the given path /// exists and `None` otherwise. - #[tracing::instrument(level = "trace", skip(self, db))] fn vendored(&self, db: &dyn Db, path: &VendoredPath) -> Result { let file = match self.inner.vendored_by_path.entry(path.to_path_buf()) { Entry::Occupied(entry) => *entry.get(), @@ -131,6 +131,7 @@ impl Files { Err(_) => return Err(FileError::NotFound), }; + tracing::trace!("Adding vendored file {}", path); let file = File::builder(FilePath::Vendored(path.to_path_buf())) .permissions(Some(0o444)) .revision(metadata.revision()) @@ -151,13 +152,14 @@ impl Files { /// For a non-existing file, creates a new salsa [`File`] ingredient and stores it for future lookups. /// /// The operations fails if the system failed to provide a metadata for the path. - #[tracing::instrument(level = "trace", skip(self, db), ret)] pub fn add_virtual_file(&self, db: &dyn Db, path: &SystemVirtualPath) -> Option { let file = match self.inner.system_virtual_by_path.entry(path.to_path_buf()) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { let metadata = db.system().virtual_path_metadata(path).ok()?; + tracing::trace!("Adding virtual file {}", path); + let file = File::builder(FilePath::SystemVirtual(path.to_path_buf())) .revision(metadata.revision()) .permissions(metadata.permissions()) @@ -207,9 +209,9 @@ impl Files { /// Refreshing the state of every file under `path` is expensive. It requires iterating over all known files /// and making system calls to get the latest status of each file in `path`. /// That's why [`File::sync_path`] and [`File::sync_path`] is preferred if it is known that the path is a file. - #[tracing::instrument(level = "debug", skip(db))] pub fn sync_recursively(db: &mut dyn Db, path: &SystemPath) { let path = SystemPath::absolute(path, db.system().current_directory()); + tracing::debug!("Syncing all files in {path}"); let inner = Arc::clone(&db.files().inner); for entry in inner.system_by_path.iter_mut() { @@ -237,8 +239,8 @@ impl Files { /// # Performance /// Refreshing the state of every file is expensive. It requires iterating over all known files and /// issuing a system call to get the latest status of each file. - #[tracing::instrument(level = "debug", skip(db))] pub fn sync_all(db: &mut dyn Db) { + tracing::debug!("Syncing all files"); let inner = Arc::clone(&db.files().inner); for entry in inner.system_by_path.iter_mut() { File::sync_system_path(db, entry.key(), Some(*entry.value())); @@ -350,7 +352,6 @@ impl File { } /// Refreshes the file metadata by querying the file system if needed. - #[tracing::instrument(level = "debug", skip(db))] pub fn sync_path(db: &mut dyn Db, path: &SystemPath) { let absolute = SystemPath::absolute(path, db.system().current_directory()); Files::touch_root(db, &absolute); @@ -358,7 +359,6 @@ impl File { } /// Syncs the [`File`]'s state with the state of the file on the system. - #[tracing::instrument(level = "debug", skip(db))] pub fn sync(self, db: &mut dyn Db) { let path = self.path(db).clone(); @@ -413,16 +413,19 @@ impl File { let durability = durability.unwrap_or_default(); if file.status(db) != status { + tracing::debug!("Updating the status of {}", file.path(db),); file.set_status(db).with_durability(durability).to(status); } if file.revision(db) != revision { + tracing::debug!("Updating the revision of {}", file.path(db)); file.set_revision(db) .with_durability(durability) .to(revision); } if file.permissions(db) != permission { + tracing::debug!("Updating the permissions of {}", file.path(db),); file.set_permissions(db) .with_durability(durability) .to(permission); diff --git a/crates/ruff_db/src/files/path.rs b/crates/ruff_db/src/files/path.rs index 816eaf461a..fddac0fa22 100644 --- a/crates/ruff_db/src/files/path.rs +++ b/crates/ruff_db/src/files/path.rs @@ -2,6 +2,7 @@ use crate::files::{system_path_to_file, vendored_path_to_file, File}; use crate::system::{SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf}; use crate::vendored::{VendoredPath, VendoredPathBuf}; use crate::Db; +use std::fmt::{Display, Formatter}; /// Path to a file. /// @@ -209,3 +210,13 @@ impl PartialEq for VendoredPathBuf { other == self } } + +impl Display for FilePath { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FilePath::System(path) => std::fmt::Display::fmt(path, f), + FilePath::SystemVirtual(path) => std::fmt::Display::fmt(path, f), + FilePath::Vendored(path) => std::fmt::Display::fmt(path, f), + } + } +} diff --git a/crates/ruff_db/src/parsed.rs b/crates/ruff_db/src/parsed.rs index 90afb1fa7b..610372b59c 100644 --- a/crates/ruff_db/src/parsed.rs +++ b/crates/ruff_db/src/parsed.rs @@ -22,7 +22,7 @@ use crate::Db; /// for determining if a query result is unchanged. #[salsa::tracked(return_ref, no_eq)] pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule { - let _span = tracing::trace_span!("parse_module", file = ?file.path(db)).entered(); + let _span = tracing::trace_span!("parsed_module", file = %file.path(db)).entered(); let source = source_text(db, file); let path = file.path(db); diff --git a/crates/ruff_db/src/source.rs b/crates/ruff_db/src/source.rs index 3bebac8e57..92b54500db 100644 --- a/crates/ruff_db/src/source.rs +++ b/crates/ruff_db/src/source.rs @@ -14,7 +14,7 @@ use crate::Db; #[salsa::tracked] pub fn source_text(db: &dyn Db, file: File) -> SourceText { let path = file.path(db); - let _span = tracing::trace_span!("source_text", file=?path).entered(); + let _span = tracing::trace_span!("source_text", file = %path).entered(); let is_notebook = match path { FilePath::System(system) => system.extension().is_some_and(|extension| { diff --git a/crates/ruff_db/src/vendored/path.rs b/crates/ruff_db/src/vendored/path.rs index 7144ae5a3d..a8cb07a672 100644 --- a/crates/ruff_db/src/vendored/path.rs +++ b/crates/ruff_db/src/vendored/path.rs @@ -1,4 +1,5 @@ use std::borrow::Borrow; +use std::fmt::Formatter; use std::ops::Deref; use std::path; @@ -197,3 +198,15 @@ impl TryFrom for VendoredPathBuf { Ok(VendoredPathBuf(camino::Utf8PathBuf::try_from(value)?)) } } + +impl std::fmt::Display for VendoredPath { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "vendored://{}", &self.0) + } +} + +impl std::fmt::Display for VendoredPathBuf { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.as_path(), f) + } +}