tracing-durations-export-0.3.0/.cargo_vcs_info.json0000644000000001360000000000100160100ustar { "git": { "sha1": "671890587f893987203d4c403305784c067310d3" }, "path_in_vcs": "" }tracing-durations-export-0.3.0/.github/workflows/test.yml000064400000000000000000000032051046102023000216770ustar 00000000000000name: Rust on: [push, pull_request] env: CARGO_TERM_COLOR: always jobs: lint: name: "Lint" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.12 - name: "Install Rustfmt" run: rustup component add rustfmt - name: "Install uv" run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: "rustfmt" run: cargo fmt --all --check - name: "Prettier" run: npx prettier --check "**/*.{md,yml}" - name: "Ruff" run: | uvx ruff check --diff . uvx ruff format --diff . clippy: name: "Clippy" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # > selecting a toolchain either by action or manual `rustup` calls should happen # > before Swatinem/rust-cache, as the cache uses the current rustc version as its cache key - name: "Install clippy" run: | rustup toolchain install stable --profile minimal rustup component add clippy - uses: Swatinem/rust-cache@v2 - run: cargo clippy - run: cargo clippy --workspace --all-features test: name: "Test" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # > selecting a toolchain either by action or manual `rustup` calls should happen # > before Swatinem/rust-cache, as the cache uses the current rustc version as its cache key - run: rustup toolchain install stable --profile minimal - uses: Swatinem/rust-cache@v2 - run: cargo test - run: cargo test --workspace --all-features tracing-durations-export-0.3.0/.gitignore000064400000000000000000000000101046102023000165570ustar 00000000000000/target tracing-durations-export-0.3.0/Cargo.lock0000644000000452520000000000100137730ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "addr2line" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "anstream" version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys", ] [[package]] name = "anyhow" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", ] [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" version = "1.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "fs-err" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" dependencies = [ "autocfg", ] [[package]] name = "futures" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "gimli" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "object" version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "pin-project-lite" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "ppv-lite86" version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "svg" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "700efb40f3f559c23c18b446e8ed62b08b56b2bb3197b36d57e0470b4102779e" [[package]] name = "syn" version = "2.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "tokio" version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "pin-project-lite", "tokio-macros", ] [[package]] name = "tokio-macros" version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] [[package]] name = "tracing-durations-export" version = "0.3.0" dependencies = [ "anyhow", "clap", "fs-err", "futures", "itertools", "once_cell", "rand", "rustc-hash", "serde", "serde_json", "svg", "tokio", "tracing", "tracing-subscriber", ] [[package]] name = "tracing-subscriber" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "sharded-slab", "thread_local", "tracing-core", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn", ] tracing-durations-export-0.3.0/Cargo.toml0000644000000046150000000000100140140ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "tracing-durations-export" version = "0.3.0" build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Record and visualize parallelism of tracing spans" readme = "Readme.md" keywords = ["tracing"] categories = [ "asynchronous", "development-tools::debugging", "development-tools::profiling", ] license = "MIT OR Apache-2.0" repository = "https://github.com/konstin/tracing-durations-export" [package.metadata.docs.rs] features = ["plot"] [lib] name = "tracing_durations_export" path = "src/lib.rs" [[bin]] name = "plot" path = "src/bin/plot.rs" required-features = [ "plot", "cli", ] [[example]] name = "cached_network" path = "examples/cached_network.rs" [dependencies.anyhow] version = "1.0.86" optional = true [dependencies.clap] version = "4.5.16" features = ["derive"] optional = true [dependencies.fs] version = "2.11.0" package = "fs-err" [dependencies.itertools] version = "0.13.0" optional = true [dependencies.once_cell] version = "1.19.0" [dependencies.rustc-hash] version = "2.0.0" optional = true [dependencies.serde] version = "1.0.208" features = ["derive"] [dependencies.serde_json] version = "1.0.125" [dependencies.svg] version = "0.17.0" optional = true [dependencies.tracing] version = "0.1.40" default-features = false [dependencies.tracing-subscriber] version = "0.3.18" default-features = false [dev-dependencies.futures] version = "0.3.30" [dev-dependencies.rand] version = "0.8.5" [dev-dependencies.tokio] version = "1.39.3" features = [ "rt-multi-thread", "macros", "sync", "time", ] [dev-dependencies.tracing] version = "0.1.40" features = ["attributes"] default-features = false [dev-dependencies.tracing-subscriber] version = "0.3.18" features = [ "fmt", "std", "registry", ] default-features = false [features] cli = ["clap"] plot = [ "anyhow", "itertools", "rustc-hash", "svg", ] tracing-durations-export-0.3.0/Cargo.toml.orig000064400000000000000000000030431046102023000174670ustar 00000000000000# cargo-features = ["public-dependency"] [package] name = "tracing-durations-export" version = "0.3.0" edition = "2021" description = "Record and visualize parallelism of tracing spans" license = "MIT OR Apache-2.0" readme = "Readme.md" repository = "https://github.com/konstin/tracing-durations-export" categories = ["asynchronous", "development-tools::debugging", "development-tools::profiling"] keywords = ["tracing"] [[bin]] name = "plot" required-features = ["plot", "cli"] [dependencies] anyhow = { version = "1.0.86", optional = true } clap = { version = "4.5.16", optional = true, features = ["derive"] } fs = { package = "fs-err", version = "2.11.0" } itertools = { version = "0.13.0", optional = true } once_cell = "1.19.0" rustc-hash = { version = "2.0.0", optional = true } serde = { version = "1.0.208", features = ["derive"] } # public = true serde_json = "1.0.125" svg = { version = "0.17.0", optional = true } # public = true tracing = { version = "0.1.40", default-features = false } # public = true tracing-subscriber = { version = "0.3.18", default-features = false } # public = true [features] plot = ["anyhow", "itertools", "rustc-hash", "svg"] cli = ["clap"] [dev-dependencies] futures = "0.3.30" rand = "0.8.5" tokio = { version = "1.39.3", features = ["rt-multi-thread", "macros", "sync", "time"] } tracing = { version = "0.1.40", default-features = false, features = ["attributes"] } tracing-subscriber = { version = "0.3.18", default-features = false, features = ["fmt", "std", "registry"] } [package.metadata.docs.rs] features = ["plot"] tracing-durations-export-0.3.0/Changelog.md000064400000000000000000000015131046102023000170110ustar 00000000000000# Changelog ## 0.3.0 - Update svg to 0.17.0 ## 0.2.0 - Show whether an active span is running on and blocking the main thread or whether it's running in a threadpool with `tokio::task::spawn_blocking`. `--color-top`/`color_top` gets split into two colors, color top main and color top threadpool. The former is used when the task is running on the main thread, the latter is used when it's offloaded to the threadpool. - Colorblind friendly default colors (http://www.cookbook-r.com/Graphs/Colors_(ggplot2)/#a-colorblind-friendly-palette): - color top blocking: #E69F0088 - color top threadpool: #56B4E988 - color bottom: #E69F0088 ## 0.1.2 - Add `--inline-field` / `inline_field` option: If the is only one field, display its value inline. Since the text is not limited to its box, text can overlap and become unreadable. tracing-durations-export-0.3.0/Readme.md000064400000000000000000000145731046102023000163310ustar 00000000000000# tracing-durations-export [![crates.io](https://img.shields.io/crates/v/tracing-durations-export.svg?logo=rust)](https://crates.io/crates/tracing-durations-export) [![Documentation](https://docs.rs/tracing-durations-export/badge.svg)](https://docs.rs/tracing-durations-export) A tracing layer to figure out which tasks are running in parallel and which are blocked on cpu, mainly for cli applications. Each span from beginning to end is a blue stripe. An async span can either be active or yield and wait ([details in the tracing docs](https://docs.rs/tracing/latest/tracing/struct.Span.html#in-asynchronous-code)), only in the sections in which a span is active, we plot an orange section above it. Sync spans are always active, so their blue and orange regions are identical. The darker the color the more spans of the same name are active at the same time. The example plot below is generated by [cached_network.rs](examples/cached_network.rs) has four sections. The first show sequentially making network requests and parsing the response, the second section shows the same logic but parallelized with [buffer_unordered](https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html#method.buffer_unordered). You can see how the requests happen in parallel and the parsing starts as soon as a request is finished. Sections three and four are a more complex example where we first check a cache before emitting a network request. ![Example plot](examples/cached_network.svg) Open the svg in your browser and hover over the sections for detailed timings and field information. The multi-lane option provides a more verbose view showing each individual span: ![Example plot, multi lane](examples/cached_network_multi_lane.svg) The plots are complementary to a cpu profiler such as `perf` or [sample](https://github.com/mstange/samply) and looking at raw span durations. They don't give you and exact work-by-line breakdown, instead they tell you where cpu is blocking or delaying other work and when the cpu is idle waiting for more parallelism. ## Usage ```rust use std::fs::File; use std::io::BufWriter; use tracing_durations_export::DurationLayer; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::{registry::Registry, fmt}; fn setup_global_subscriber() -> DurationsLayerDropGuard { let fmt_layer = fmt::Layer::default(); let (duration_layer, guard) = DurationsLayerBuilder::default() .durations_file("traces.ndjson") // Available with the `plot` feature // .plot_file("traces.svg") .build() .unwrap(); let subscriber = tracing_subscriber::registry() .with(fmt_layer) .with(duration_layer) .init(); guard } // your code here ... ``` You can either use the `plot` feature and set `.plot_file()` on the builder, or after running your application, run ```shell cargo run --bin plot --features plot --features cli -- traces.ndjson ``` and open `traces.svg`. For the plots at the beginning of the readme: ```shell TRACING_DURATION_EXPORT=examples/cached_network.ndjson cargo run --example cached_network cargo run --bin plot --features plot --features cli -- examples/cached_network.ndjson cargo run --bin plot --features plot --features cli -- --multi-lane examples/cached_network.ndjson --output examples/cached_network_multi_lane.svg ``` The `traces.ndjson` output file will look something like below, where each section where a span is active is one line. ````ndjson [...] {"id":6,"name":"read_cache","start":{"secs":0,"nanos":122457871},"end":{"secs":0,"nanos":122463135},"parents":[5],"fields":{"id":"2"}} {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":122433854},"end":{"secs":0,"nanos":122499689},"parents":[],"fields":{"id":"2","api":"https://example.net/cached"}} {"id":9007474132647937,"name":"parse_cache","start":{"secs":0,"nanos":122625724},"end":{"secs":0,"nanos":125791908},"parents":[],"fields":{}} {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":125973025},"end":{"secs":0,"nanos":126007737},"parents":[],"fields":{"id":"2","api":"https://example.net/cached"}} {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":126061739},"end":{"secs":0,"nanos":126066912},"parents":[],"fields":{"id":"2","api":"https://example.net/cached"}} {"id":2251799813685254,"name":"read_cache","start":{"secs":0,"nanos":126157156},"end":{"secs":0,"nanos":126193547},"parents":[2251799813685253],"fields":{"id":"3"}} {"id":2251799813685253,"name":"cached_network_request","start":{"secs":0,"nanos":126144140},"end":{"secs":0,"nanos":126213181},"parents":[],"fields":{"api":"https://example.net/cached","id":"3"}} {"id":27021597764222977,"name":"make_network_request","start":{"secs":0,"nanos":128343009},"end":{"secs":0,"nanos":128383121},"parents":[13510798882111491],"fields":{"api":"https://example.net/cached","id":"0"}}``` [...] ```` Note that 0 is the time of the first span, not the start of the process. ## Case Study This is an example from the dependency resolver [uv](https://github.com/astral-sh/uv). We make a guess about what versions fit the user's constraints, and then has to fetch the metadata for them to check if those versions are compatible. In this particular case, we have to try a lot of versions for two packages called `boto3` and `botocoro`. Previously, trying would happen sequentially: ![A plot without much parallelism, 43s total](examples/uv_1.png) We can optimize this by speculating: We make a guess about what versions could fit and also what version we would try next if that didn't work out, and fetch in parallel ([Pull request](https://github.com/astral-sh/uv/pull/2452)): ![A plot with some parallelism, but in spikes with non-parallel sections in between, 25s total](examples/uv_2.png) This is faster, but you can see we're having phases of a lot of parallelism with sequential regions in between. Hovering over the spans in the svg version in a browser, we could see that we were only prefetching `boto3` in parallel, but not `botocore`. With a fix that prefetches both: ![A with a lot parallelism, 2s total](examples/uv_3.png) Now we're taking 2s instead of 43s, so the initial section expanded, but what you can mainly see is that we're highly parallel now. Another case study is [this pull request](https://github.com/astral-sh/uv/pull/1163), where we replaced a sync channel with an async channel, and the yielding would unlock parallelism. You can also see the impact of `spawn_blocking` there. tracing-durations-export-0.3.0/examples/cached_network.ndjson000064400000000000000000000540151046102023000226200ustar 00000000000000{"id":1,"name":"make_network_request","start":{"secs":0,"nanos":198},"end":{"secs":0,"nanos":36948},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.org/uncached","id":"0"}} {"id":1,"name":"make_network_request","start":{"secs":0,"nanos":8135031},"end":{"secs":0,"nanos":8154215},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.org/uncached","id":"0"}} {"id":1,"name":"make_network_request","start":{"secs":0,"nanos":8165141},"end":{"secs":0,"nanos":8166459},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.org/uncached","id":"0"}} {"id":274877906945,"name":"parse_network","start":{"secs":0,"nanos":8299099},"end":{"secs":0,"nanos":15405966},"parents":[],"is_main_thread":false,"fields":{}} {"id":2251799813685249,"name":"make_network_request","start":{"secs":0,"nanos":15473668},"end":{"secs":0,"nanos":15490326},"parents":[],"is_main_thread":true,"fields":{"id":"1","api":"https://example.org/uncached"}} {"id":2251799813685249,"name":"make_network_request","start":{"secs":0,"nanos":21569228},"end":{"secs":0,"nanos":21578020},"parents":[],"is_main_thread":true,"fields":{"id":"1","api":"https://example.org/uncached"}} {"id":2251799813685249,"name":"make_network_request","start":{"secs":0,"nanos":21583703},"end":{"secs":0,"nanos":21584751},"parents":[],"is_main_thread":true,"fields":{"id":"1","api":"https://example.org/uncached"}} {"id":2252074691592193,"name":"parse_network","start":{"secs":0,"nanos":21613217},"end":{"secs":0,"nanos":27674084},"parents":[],"is_main_thread":false,"fields":{}} {"id":4503599627370497,"name":"make_network_request","start":{"secs":0,"nanos":27699330},"end":{"secs":0,"nanos":27705836},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.org/uncached"}} {"id":4503599627370497,"name":"make_network_request","start":{"secs":0,"nanos":37771322},"end":{"secs":0,"nanos":37776294},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.org/uncached"}} {"id":4503599627370497,"name":"make_network_request","start":{"secs":0,"nanos":37780767},"end":{"secs":0,"nanos":37781920},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.org/uncached"}} {"id":4503874505277441,"name":"parse_network","start":{"secs":0,"nanos":37803954},"end":{"secs":0,"nanos":43864185},"parents":[],"is_main_thread":false,"fields":{}} {"id":6755399441055745,"name":"make_network_request","start":{"secs":0,"nanos":43897729},"end":{"secs":0,"nanos":43907998},"parents":[],"is_main_thread":true,"fields":{"id":"3","api":"https://example.org/uncached"}} {"id":6755399441055745,"name":"make_network_request","start":{"secs":0,"nanos":50979991},"end":{"secs":0,"nanos":50991661},"parents":[],"is_main_thread":true,"fields":{"id":"3","api":"https://example.org/uncached"}} {"id":6755399441055745,"name":"make_network_request","start":{"secs":0,"nanos":51000533},"end":{"secs":0,"nanos":51003295},"parents":[],"is_main_thread":true,"fields":{"id":"3","api":"https://example.org/uncached"}} {"id":6755674318962689,"name":"parse_network","start":{"secs":0,"nanos":51043909},"end":{"secs":0,"nanos":58113987},"parents":[],"is_main_thread":false,"fields":{}} {"id":9007199254740993,"name":"make_network_request","start":{"secs":0,"nanos":64382647},"end":{"secs":0,"nanos":64402345},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.org/uncached","id":"0"}} {"id":2,"name":"make_network_request","start":{"secs":0,"nanos":64435856},"end":{"secs":0,"nanos":64447280},"parents":[],"is_main_thread":true,"fields":{"id":"1","api":"https://example.org/uncached"}} {"id":3,"name":"make_network_request","start":{"secs":0,"nanos":64469999},"end":{"secs":0,"nanos":64478101},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.org/uncached"}} {"id":4,"name":"make_network_request","start":{"secs":0,"nanos":64499842},"end":{"secs":0,"nanos":64507954},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.org/uncached","id":"3"}} {"id":2,"name":"make_network_request","start":{"secs":0,"nanos":71541150},"end":{"secs":0,"nanos":71552229},"parents":[],"is_main_thread":true,"fields":{"id":"1","api":"https://example.org/uncached"}} {"id":2,"name":"make_network_request","start":{"secs":0,"nanos":71566408},"end":{"secs":0,"nanos":71570798},"parents":[],"is_main_thread":true,"fields":{"id":"1","api":"https://example.org/uncached"}} {"id":3,"name":"make_network_request","start":{"secs":0,"nanos":72610379},"end":{"secs":0,"nanos":72619173},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.org/uncached"}} {"id":3,"name":"make_network_request","start":{"secs":0,"nanos":72632614},"end":{"secs":0,"nanos":72637175},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.org/uncached"}} {"id":9007199254740993,"name":"make_network_request","start":{"secs":0,"nanos":72795813},"end":{"secs":0,"nanos":72803951},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.org/uncached","id":"0"}} {"id":9007199254740993,"name":"make_network_request","start":{"secs":0,"nanos":72817681},"end":{"secs":0,"nanos":72822015},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.org/uncached","id":"0"}} {"id":4,"name":"make_network_request","start":{"secs":0,"nanos":72907246},"end":{"secs":0,"nanos":72914500},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.org/uncached","id":"3"}} {"id":4,"name":"make_network_request","start":{"secs":0,"nanos":72928215},"end":{"secs":0,"nanos":72932557},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.org/uncached","id":"3"}} {"id":9007474132647937,"name":"parse_network","start":{"secs":0,"nanos":71620270},"end":{"secs":0,"nanos":77697967},"parents":[],"is_main_thread":false,"fields":{}} {"id":1099511627777,"name":"parse_network","start":{"secs":0,"nanos":73208205},"end":{"secs":0,"nanos":79354195},"parents":[],"is_main_thread":false,"fields":{}} {"id":824633720833,"name":"parse_network","start":{"secs":0,"nanos":73197110},"end":{"secs":0,"nanos":79371917},"parents":[],"is_main_thread":false,"fields":{}} {"id":549755813889,"name":"parse_network","start":{"secs":0,"nanos":73062119},"end":{"secs":0,"nanos":80228553},"parents":[],"is_main_thread":false,"fields":{}} {"id":11258999068426241,"name":"read_cache","start":{"secs":0,"nanos":86454407},"end":{"secs":0,"nanos":86472708},"parents":[2251799813685252],"is_main_thread":true,"fields":{"id":"0"}} {"id":2251799813685252,"name":"cached_network_request","start":{"secs":0,"nanos":86434322},"end":{"secs":0,"nanos":86491852},"parents":[],"is_main_thread":true,"fields":{"id":"0","api":"https://example.net/cached"}} {"id":11258999068426241,"name":"read_cache","start":{"secs":0,"nanos":88549229},"end":{"secs":0,"nanos":88559990},"parents":[2251799813685252],"is_main_thread":true,"fields":{"id":"0"}} {"id":11258999068426241,"name":"read_cache","start":{"secs":0,"nanos":88572038},"end":{"secs":0,"nanos":88577453},"parents":[2251799813685252],"is_main_thread":true,"fields":{"id":"0"}} {"id":2251799813685252,"name":"cached_network_request","start":{"secs":0,"nanos":88546784},"end":{"secs":0,"nanos":88609558},"parents":[],"is_main_thread":true,"fields":{"id":"0","api":"https://example.net/cached"}} {"id":11259273946333185,"name":"parse_cache","start":{"secs":0,"nanos":88633605},"end":{"secs":0,"nanos":92706376},"parents":[],"is_main_thread":false,"fields":{}} {"id":2251799813685252,"name":"cached_network_request","start":{"secs":0,"nanos":92750190},"end":{"secs":0,"nanos":92762696},"parents":[],"is_main_thread":true,"fields":{"id":"0","api":"https://example.net/cached"}} {"id":2251799813685252,"name":"cached_network_request","start":{"secs":0,"nanos":92777052},"end":{"secs":0,"nanos":92781910},"parents":[],"is_main_thread":true,"fields":{"id":"0","api":"https://example.net/cached"}} {"id":13510798882111489,"name":"read_cache","start":{"secs":0,"nanos":92826698},"end":{"secs":0,"nanos":92843389},"parents":[4503599627370500],"is_main_thread":true,"fields":{"id":"1"}} {"id":4503599627370500,"name":"cached_network_request","start":{"secs":0,"nanos":92815547},"end":{"secs":0,"nanos":92857556},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"1"}} {"id":13510798882111489,"name":"read_cache","start":{"secs":0,"nanos":95932273},"end":{"secs":0,"nanos":95948383},"parents":[4503599627370500],"is_main_thread":true,"fields":{"id":"1"}} {"id":13510798882111489,"name":"read_cache","start":{"secs":0,"nanos":95960266},"end":{"secs":0,"nanos":95965746},"parents":[4503599627370500],"is_main_thread":true,"fields":{"id":"1"}} {"id":4503599627370500,"name":"cached_network_request","start":{"secs":0,"nanos":95929242},"end":{"secs":0,"nanos":95992805},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"1"}} {"id":2252899325313025,"name":"parse_cache","start":{"secs":0,"nanos":96008936},"end":{"secs":0,"nanos":100078731},"parents":[],"is_main_thread":false,"fields":{}} {"id":4503599627370500,"name":"cached_network_request","start":{"secs":0,"nanos":100114987},"end":{"secs":0,"nanos":100124529},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"1"}} {"id":4503599627370500,"name":"cached_network_request","start":{"secs":0,"nanos":100138491},"end":{"secs":0,"nanos":100142953},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"1"}} {"id":15762598695796737,"name":"read_cache","start":{"secs":0,"nanos":100183670},"end":{"secs":0,"nanos":100198864},"parents":[6755399441055748],"is_main_thread":true,"fields":{"id":"2"}} {"id":6755399441055748,"name":"cached_network_request","start":{"secs":0,"nanos":100172582},"end":{"secs":0,"nanos":100212968},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"2"}} {"id":15762598695796737,"name":"read_cache","start":{"secs":0,"nanos":103284078},"end":{"secs":0,"nanos":103294444},"parents":[6755399441055748],"is_main_thread":true,"fields":{"id":"2"}} {"id":15762598695796737,"name":"read_cache","start":{"secs":0,"nanos":103305977},"end":{"secs":0,"nanos":103311313},"parents":[6755399441055748],"is_main_thread":true,"fields":{"id":"2"}} {"id":6755399441055748,"name":"cached_network_request","start":{"secs":0,"nanos":103281653},"end":{"secs":0,"nanos":103334663},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"2"}} {"id":2252624447406081,"name":"parse_cache","start":{"secs":0,"nanos":103373804},"end":{"secs":0,"nanos":105449352},"parents":[],"is_main_thread":false,"fields":{}} {"id":6755399441055748,"name":"cached_network_request","start":{"secs":0,"nanos":105515624},"end":{"secs":0,"nanos":105523752},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"2"}} {"id":6755399441055748,"name":"cached_network_request","start":{"secs":0,"nanos":105536847},"end":{"secs":0,"nanos":105541308},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"2"}} {"id":18014398509481985,"name":"read_cache","start":{"secs":0,"nanos":105576610},"end":{"secs":0,"nanos":105591397},"parents":[9007199254740996],"is_main_thread":true,"fields":{"id":"3"}} {"id":9007199254740996,"name":"cached_network_request","start":{"secs":0,"nanos":105567040},"end":{"secs":0,"nanos":105605830},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"3"}} {"id":18014398509481985,"name":"read_cache","start":{"secs":0,"nanos":107680188},"end":{"secs":0,"nanos":107690523},"parents":[9007199254740996],"is_main_thread":true,"fields":{"id":"3"}} {"id":18014398509481985,"name":"read_cache","start":{"secs":0,"nanos":107702391},"end":{"secs":0,"nanos":107708479},"parents":[9007199254740996],"is_main_thread":true,"fields":{"id":"3"}} {"id":20266198323167233,"name":"make_network_request","start":{"secs":0,"nanos":107734629},"end":{"secs":0,"nanos":107747271},"parents":[9007199254740996],"is_main_thread":true,"fields":{"id":"3","api":"https://example.net/cached"}} {"id":9007199254740996,"name":"cached_network_request","start":{"secs":0,"nanos":107677834},"end":{"secs":0,"nanos":107763664},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"3"}} {"id":20266198323167233,"name":"make_network_request","start":{"secs":0,"nanos":113848867},"end":{"secs":0,"nanos":113859227},"parents":[9007199254740996],"is_main_thread":true,"fields":{"id":"3","api":"https://example.net/cached"}} {"id":20266198323167233,"name":"make_network_request","start":{"secs":0,"nanos":113872887},"end":{"secs":0,"nanos":113878411},"parents":[9007199254740996],"is_main_thread":true,"fields":{"id":"3","api":"https://example.net/cached"}} {"id":9007199254740996,"name":"cached_network_request","start":{"secs":0,"nanos":113829230},"end":{"secs":0,"nanos":113906454},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"3"}} {"id":2252349569499137,"name":"parse_network","start":{"secs":0,"nanos":113982386},"end":{"secs":0,"nanos":119055002},"parents":[],"is_main_thread":false,"fields":{}} {"id":9007199254740996,"name":"cached_network_request","start":{"secs":0,"nanos":119094851},"end":{"secs":0,"nanos":119105779},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"3"}} {"id":9007199254740996,"name":"cached_network_request","start":{"secs":0,"nanos":119119105},"end":{"secs":0,"nanos":119123432},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"3"}} {"id":22517998136852481,"name":"read_cache","start":{"secs":0,"nanos":125282501},"end":{"secs":0,"nanos":125296336},"parents":[11258999068426244],"is_main_thread":true,"fields":{"id":"0"}} {"id":11258999068426244,"name":"cached_network_request","start":{"secs":0,"nanos":125272627},"end":{"secs":0,"nanos":125311304},"parents":[],"is_main_thread":true,"fields":{"id":"0","api":"https://example.net/cached"}} {"id":2251799813685250,"name":"read_cache","start":{"secs":0,"nanos":125342410},"end":{"secs":0,"nanos":125352856},"parents":[2251799813685251],"is_main_thread":true,"fields":{"id":"1"}} {"id":2251799813685251,"name":"cached_network_request","start":{"secs":0,"nanos":125333309},"end":{"secs":0,"nanos":125366481},"parents":[],"is_main_thread":true,"fields":{"id":"1","api":"https://example.net/cached"}} {"id":6,"name":"read_cache","start":{"secs":0,"nanos":125396640},"end":{"secs":0,"nanos":125405840},"parents":[5],"is_main_thread":true,"fields":{"id":"2"}} {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":125387338},"end":{"secs":0,"nanos":125419520},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.net/cached"}} {"id":22517998136852481,"name":"read_cache","start":{"secs":0,"nanos":127497643},"end":{"secs":0,"nanos":127540558},"parents":[11258999068426244],"is_main_thread":true,"fields":{"id":"0"}} {"id":22517998136852481,"name":"read_cache","start":{"secs":0,"nanos":127569231},"end":{"secs":0,"nanos":127575129},"parents":[11258999068426244],"is_main_thread":true,"fields":{"id":"0"}} {"id":24769797950537729,"name":"make_network_request","start":{"secs":0,"nanos":127640962},"end":{"secs":0,"nanos":127673420},"parents":[11258999068426244],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"0"}} {"id":11258999068426244,"name":"cached_network_request","start":{"secs":0,"nanos":127490435},"end":{"secs":0,"nanos":127690287},"parents":[],"is_main_thread":true,"fields":{"id":"0","api":"https://example.net/cached"}} {"id":2251799813685250,"name":"read_cache","start":{"secs":0,"nanos":127709477},"end":{"secs":0,"nanos":127718892},"parents":[2251799813685251],"is_main_thread":true,"fields":{"id":"1"}} {"id":2251799813685250,"name":"read_cache","start":{"secs":0,"nanos":127730268},"end":{"secs":0,"nanos":127735671},"parents":[2251799813685251],"is_main_thread":true,"fields":{"id":"1"}} {"id":2251799813685251,"name":"cached_network_request","start":{"secs":0,"nanos":127707422},"end":{"secs":0,"nanos":127769306},"parents":[],"is_main_thread":true,"fields":{"id":"1","api":"https://example.net/cached"}} {"id":6,"name":"read_cache","start":{"secs":0,"nanos":127788598},"end":{"secs":0,"nanos":127849239},"parents":[5],"is_main_thread":true,"fields":{"id":"2"}} {"id":6,"name":"read_cache","start":{"secs":0,"nanos":127860517},"end":{"secs":0,"nanos":127865573},"parents":[5],"is_main_thread":true,"fields":{"id":"2"}} {"id":2251799813685254,"name":"make_network_request","start":{"secs":0,"nanos":127939940},"end":{"secs":0,"nanos":127954727},"parents":[5],"is_main_thread":true,"fields":{"id":"2","api":"https://example.net/cached"}} {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":127784745},"end":{"secs":0,"nanos":127970479},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.net/cached"}} {"id":13511073760018433,"name":"parse_cache","start":{"secs":0,"nanos":127786379},"end":{"secs":0,"nanos":131854184},"parents":[],"is_main_thread":false,"fields":{}} {"id":2251799813685251,"name":"cached_network_request","start":{"secs":0,"nanos":131973484},"end":{"secs":0,"nanos":132011093},"parents":[],"is_main_thread":true,"fields":{"id":"1","api":"https://example.net/cached"}} {"id":2251799813685251,"name":"cached_network_request","start":{"secs":0,"nanos":132041097},"end":{"secs":0,"nanos":132046176},"parents":[],"is_main_thread":true,"fields":{"id":"1","api":"https://example.net/cached"}} {"id":4503599627370498,"name":"read_cache","start":{"secs":0,"nanos":132152201},"end":{"secs":0,"nanos":132196092},"parents":[4503599627370499],"is_main_thread":true,"fields":{"id":"3"}} {"id":4503599627370499,"name":"cached_network_request","start":{"secs":0,"nanos":132121755},"end":{"secs":0,"nanos":132213040},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"3"}} {"id":4503599627370498,"name":"read_cache","start":{"secs":0,"nanos":134371539},"end":{"secs":0,"nanos":134412688},"parents":[4503599627370499],"is_main_thread":true,"fields":{"id":"3"}} {"id":4503599627370498,"name":"read_cache","start":{"secs":0,"nanos":134441039},"end":{"secs":0,"nanos":134446928},"parents":[4503599627370499],"is_main_thread":true,"fields":{"id":"3"}} {"id":6755399441055746,"name":"make_network_request","start":{"secs":0,"nanos":134510498},"end":{"secs":0,"nanos":134534141},"parents":[4503599627370499],"is_main_thread":true,"fields":{"id":"3","api":"https://example.net/cached"}} {"id":4503599627370499,"name":"cached_network_request","start":{"secs":0,"nanos":134364473},"end":{"secs":0,"nanos":134551333},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"3"}} {"id":2251799813685254,"name":"make_network_request","start":{"secs":0,"nanos":135435642},"end":{"secs":0,"nanos":135477193},"parents":[5],"is_main_thread":true,"fields":{"id":"2","api":"https://example.net/cached"}} {"id":2251799813685254,"name":"make_network_request","start":{"secs":0,"nanos":135507818},"end":{"secs":0,"nanos":135514216},"parents":[5],"is_main_thread":true,"fields":{"id":"2","api":"https://example.net/cached"}} {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":135428656},"end":{"secs":0,"nanos":135567885},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.net/cached"}} {"id":24769797950537729,"name":"make_network_request","start":{"secs":0,"nanos":136493658},"end":{"secs":0,"nanos":136531210},"parents":[11258999068426244],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"0"}} {"id":24769797950537729,"name":"make_network_request","start":{"secs":0,"nanos":136560236},"end":{"secs":0,"nanos":136566267},"parents":[11258999068426244],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"0"}} {"id":11258999068426244,"name":"cached_network_request","start":{"secs":0,"nanos":136487330},"end":{"secs":0,"nanos":136614926},"parents":[],"is_main_thread":true,"fields":{"id":"0","api":"https://example.net/cached"}} {"id":4504699138998273,"name":"parse_network","start":{"secs":0,"nanos":135585060},"end":{"secs":0,"nanos":139657668},"parents":[],"is_main_thread":false,"fields":{}} {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":139784041},"end":{"secs":0,"nanos":139820339},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.net/cached"}} {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":139849779},"end":{"secs":0,"nanos":139854759},"parents":[],"is_main_thread":true,"fields":{"id":"2","api":"https://example.net/cached"}} {"id":4504424261091329,"name":"parse_network","start":{"secs":0,"nanos":136655848},"end":{"secs":0,"nanos":140739562},"parents":[],"is_main_thread":false,"fields":{}} {"id":11258999068426244,"name":"cached_network_request","start":{"secs":0,"nanos":140885396},"end":{"secs":0,"nanos":140922067},"parents":[],"is_main_thread":true,"fields":{"id":"0","api":"https://example.net/cached"}} {"id":11258999068426244,"name":"cached_network_request","start":{"secs":0,"nanos":140952184},"end":{"secs":0,"nanos":140957275},"parents":[],"is_main_thread":true,"fields":{"id":"0","api":"https://example.net/cached"}} {"id":6755399441055746,"name":"make_network_request","start":{"secs":0,"nanos":143572834},"end":{"secs":0,"nanos":143614374},"parents":[4503599627370499],"is_main_thread":true,"fields":{"id":"3","api":"https://example.net/cached"}} {"id":6755399441055746,"name":"make_network_request","start":{"secs":0,"nanos":143645371},"end":{"secs":0,"nanos":143651467},"parents":[4503599627370499],"is_main_thread":true,"fields":{"id":"3","api":"https://example.net/cached"}} {"id":4503599627370499,"name":"cached_network_request","start":{"secs":0,"nanos":143565719},"end":{"secs":0,"nanos":143704690},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"3"}} {"id":4504149383184385,"name":"parse_network","start":{"secs":0,"nanos":143738502},"end":{"secs":0,"nanos":146818844},"parents":[],"is_main_thread":false,"fields":{}} {"id":4503599627370499,"name":"cached_network_request","start":{"secs":0,"nanos":146951490},"end":{"secs":0,"nanos":146988031},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"3"}} {"id":4503599627370499,"name":"cached_network_request","start":{"secs":0,"nanos":147018124},"end":{"secs":0,"nanos":147023079},"parents":[],"is_main_thread":true,"fields":{"api":"https://example.net/cached","id":"3"}} tracing-durations-export-0.3.0/examples/cached_network.rs000064400000000000000000000067111046102023000217510ustar 00000000000000use futures::StreamExt; use rand::Rng; use std::env; use std::time::Duration; use tokio::task::spawn_blocking; use tracing::instrument; use tracing_durations_export::DurationsLayerBuilder; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; #[instrument] async fn make_network_request(api: &str, id: usize) -> String { let millis = rand::thread_rng().gen_range(5..10); tokio::time::sleep(Duration::from_millis(millis)).await; format!("{api} {id}") } #[instrument] async fn read_cache(id: usize) -> Option { let millis = rand::thread_rng().gen_range(1..3); tokio::time::sleep(Duration::from_millis(millis)).await; // There's a 50% change there's a cache entry if rand::thread_rng().gen_bool(0.5) { Some(format!("cached({id})")) } else { None } } /// cpu intensive, blocking method #[instrument(skip_all)] fn parse_cache(data: &str) -> String { let millis = rand::thread_rng().gen_range(2..6); std::thread::sleep(Duration::from_millis(millis)); format!("from_cache({data})") } /// cpu intensive, blocking method #[instrument(skip_all)] fn parse_network(data: &str) -> String { let millis = rand::thread_rng().gen_range(3..8); std::thread::sleep(Duration::from_millis(millis)); format!("from_network({data})") } #[instrument] async fn cached_network_request(api: &str, id: usize) -> String { if let Some(cached) = read_cache(id).await { spawn_blocking(move || parse_cache(&cached)) .await .expect("executor died") } else { let response = make_network_request(api, id).await; spawn_blocking(move || parse_network(&response)) .await .expect("executor died") } } #[tokio::main] async fn main() { let (duration_layer, _guard) = if let Ok(location) = env::var("TRACING_DURATION_EXPORT") { let (layer, guard) = DurationsLayerBuilder::default() .durations_file(location) .build() .expect("Couldn't create TRACING_DURATION_FILE"); (Some(layer), Some(guard)) } else { (None, None) }; tracing_subscriber::registry().with(duration_layer).init(); // Sequential futures::stream::iter(0..4) .then(|id| make_network_request("https://example.org/uncached", id)) .then(|data| async { spawn_blocking(move || parse_network(&data)) .await .expect("the executor is broken") }) .collect::>() .await; // Spacer tokio::time::sleep(Duration::from_millis(5)).await; // Parallel futures::stream::iter(0..4) .map(|id| async move { let data = make_network_request("https://example.org/uncached", id).await; spawn_blocking(move || parse_network(&data)) .await .expect("the executor is broken") }) .buffer_unordered(4) .collect::>() .await; tokio::time::sleep(Duration::from_millis(5)).await; // Sequential futures::stream::iter(0..4) .then(|id| cached_network_request("https://example.net/cached", id)) .collect::>() .await; tokio::time::sleep(Duration::from_millis(5)).await; // Parallel futures::stream::iter(0..4) .map(|id| cached_network_request("https://example.net/cached", id)) .buffer_unordered(3) .collect::>() .await; } tracing-durations-export-0.3.0/examples/cached_network.svg000064400000000000000000000566501046102023000221330ustar 00000000000000 0s 0.147s cached_network_request read_cache parse_cache make_network_request parse_network make_network_request 0.000s id: 0 api: https://example.org/uncached make_network_request 0.000s api: https://example.org/uncached id: 0 make_network_request 0.000s api: https://example.org/uncached id: 0 parse_network 0.007s make_network_request 0.000s api: https://example.org/uncached id: 1 make_network_request 0.000s api: https://example.org/uncached id: 1 make_network_request 0.000s api: https://example.org/uncached id: 1 parse_network 0.006s make_network_request 0.000s api: https://example.org/uncached id: 2 make_network_request 0.000s api: https://example.org/uncached id: 2 make_network_request 0.000s id: 2 api: https://example.org/uncached parse_network 0.006s make_network_request 0.000s api: https://example.org/uncached id: 3 make_network_request 0.000s id: 3 api: https://example.org/uncached make_network_request 0.000s api: https://example.org/uncached id: 3 parse_network 0.007s make_network_request 0.000s id: 0 api: https://example.org/uncached make_network_request 0.000s id: 1 api: https://example.org/uncached make_network_request 0.000s api: https://example.org/uncached id: 2 make_network_request 0.000s api: https://example.org/uncached id: 3 make_network_request 0.000s id: 1 api: https://example.org/uncached make_network_request 0.000s id: 1 api: https://example.org/uncached make_network_request 0.000s id: 2 api: https://example.org/uncached make_network_request 0.000s id: 2 api: https://example.org/uncached make_network_request 0.000s api: https://example.org/uncached id: 0 make_network_request 0.000s api: https://example.org/uncached id: 0 make_network_request 0.000s api: https://example.org/uncached id: 3 make_network_request 0.000s api: https://example.org/uncached id: 3 parse_network 0.006s parse_network 0.006s parse_network 0.006s parse_network 0.007s read_cache 0.000s id: 0 cached_network_request 0.000s id: 0 api: https://example.net/cached read_cache 0.000s id: 0 read_cache 0.000s id: 0 cached_network_request 0.000s id: 0 api: https://example.net/cached parse_cache 0.004s cached_network_request 0.000s id: 0 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 0 read_cache 0.000s id: 1 cached_network_request 0.000s id: 1 api: https://example.net/cached read_cache 0.000s id: 1 read_cache 0.000s id: 1 cached_network_request 0.000s api: https://example.net/cached id: 1 parse_cache 0.004s cached_network_request 0.000s id: 1 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 1 read_cache 0.000s id: 2 cached_network_request 0.000s api: https://example.net/cached id: 2 read_cache 0.000s id: 2 read_cache 0.000s id: 2 cached_network_request 0.000s api: https://example.net/cached id: 2 parse_cache 0.002s cached_network_request 0.000s id: 2 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 2 read_cache 0.000s id: 3 cached_network_request 0.000s id: 3 api: https://example.net/cached read_cache 0.000s id: 3 read_cache 0.000s id: 3 make_network_request 0.000s id: 3 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 3 make_network_request 0.000s id: 3 api: https://example.net/cached make_network_request 0.000s id: 3 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 3 parse_network 0.005s cached_network_request 0.000s id: 3 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 3 read_cache 0.000s id: 0 cached_network_request 0.000s api: https://example.net/cached id: 0 read_cache 0.000s id: 1 cached_network_request 0.000s id: 1 api: https://example.net/cached read_cache 0.000s id: 2 cached_network_request 0.000s api: https://example.net/cached id: 2 read_cache 0.000s id: 0 read_cache 0.000s id: 0 make_network_request 0.000s api: https://example.net/cached id: 0 cached_network_request 0.000s api: https://example.net/cached id: 0 read_cache 0.000s id: 1 read_cache 0.000s id: 1 cached_network_request 0.000s id: 1 api: https://example.net/cached read_cache 0.000s id: 2 read_cache 0.000s id: 2 make_network_request 0.000s api: https://example.net/cached id: 2 cached_network_request 0.000s api: https://example.net/cached id: 2 parse_cache 0.004s cached_network_request 0.000s id: 1 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 1 read_cache 0.000s id: 3 cached_network_request 0.000s api: https://example.net/cached id: 3 read_cache 0.000s id: 3 read_cache 0.000s id: 3 make_network_request 0.000s api: https://example.net/cached id: 3 cached_network_request 0.000s api: https://example.net/cached id: 3 make_network_request 0.000s api: https://example.net/cached id: 2 make_network_request 0.000s api: https://example.net/cached id: 2 cached_network_request 0.000s id: 2 api: https://example.net/cached make_network_request 0.000s id: 0 api: https://example.net/cached make_network_request 0.000s api: https://example.net/cached id: 0 cached_network_request 0.000s api: https://example.net/cached id: 0 parse_network 0.004s cached_network_request 0.000s id: 2 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 2 parse_network 0.004s cached_network_request 0.000s id: 0 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 0 make_network_request 0.000s id: 3 api: https://example.net/cached make_network_request 0.000s api: https://example.net/cached id: 3 cached_network_request 0.000s id: 3 api: https://example.net/cached parse_network 0.003s cached_network_request 0.000s id: 3 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 3 cached_network_request 0.007s id: 1 api: https://example.net/cached make_network_request 0.008s api: https://example.net/cached id: 2 cached_network_request 0.015s api: https://example.net/cached id: 3 parse_cache 0.004s parse_network 0.004s parse_network 0.004s parse_network 0.003s cached_network_request 0.006s id: 0 api: https://example.net/cached parse_network 0.006s parse_network 0.007s read_cache 0.002s id: 0 parse_cache 0.004s read_cache 0.003s id: 1 cached_network_request 0.007s id: 1 api: https://example.net/cached parse_cache 0.004s read_cache 0.003s id: 2 cached_network_request 0.005s api: https://example.net/cached id: 2 parse_cache 0.002s read_cache 0.002s id: 3 cached_network_request 0.014s id: 3 api: https://example.net/cached make_network_request 0.006s id: 3 api: https://example.net/cached make_network_request 0.008s api: https://example.org/uncached id: 3 parse_network 0.006s parse_network 0.006s parse_network 0.006s make_network_request 0.010s api: https://example.org/uncached id: 2 parse_network 0.006s make_network_request 0.007s api: https://example.org/uncached id: 3 make_network_request 0.008s id: 0 api: https://example.org/uncached make_network_request 0.007s id: 1 api: https://example.org/uncached parse_network 0.007s make_network_request 0.006s api: https://example.org/uncached id: 1 parse_network 0.007s make_network_request 0.008s id: 0 api: https://example.org/uncached parse_network 0.005s read_cache 0.002s id: 0 cached_network_request 0.016s api: https://example.net/cached id: 0 read_cache 0.002s id: 1 cached_network_request 0.014s api: https://example.net/cached id: 2 make_network_request 0.009s api: https://example.net/cached id: 0 read_cache 0.002s id: 3 make_network_request 0.009s api: https://example.net/cached id: 3 read_cache 0.002s id: 2 make_network_request 0.008s api: https://example.org/uncached id: 2 tracing-durations-export-0.3.0/examples/cached_network_multi_lane.svg000064400000000000000000000567311046102023000243440ustar 00000000000000 0s 0.147s cached_network_request read_cache parse_cache make_network_request parse_network make_network_request 0.000s api: https://example.org/uncached id: 0 make_network_request 0.000s api: https://example.org/uncached id: 0 make_network_request 0.000s api: https://example.org/uncached id: 0 parse_network 0.007s make_network_request 0.000s api: https://example.org/uncached id: 1 make_network_request 0.000s api: https://example.org/uncached id: 1 make_network_request 0.000s id: 1 api: https://example.org/uncached parse_network 0.006s make_network_request 0.000s id: 2 api: https://example.org/uncached make_network_request 0.000s id: 2 api: https://example.org/uncached make_network_request 0.000s id: 2 api: https://example.org/uncached parse_network 0.006s make_network_request 0.000s id: 3 api: https://example.org/uncached make_network_request 0.000s api: https://example.org/uncached id: 3 make_network_request 0.000s id: 3 api: https://example.org/uncached parse_network 0.007s make_network_request 0.000s id: 0 api: https://example.org/uncached make_network_request 0.000s id: 1 api: https://example.org/uncached make_network_request 0.000s api: https://example.org/uncached id: 2 make_network_request 0.000s api: https://example.org/uncached id: 3 make_network_request 0.000s id: 1 api: https://example.org/uncached make_network_request 0.000s api: https://example.org/uncached id: 1 make_network_request 0.000s id: 2 api: https://example.org/uncached make_network_request 0.000s id: 2 api: https://example.org/uncached make_network_request 0.000s api: https://example.org/uncached id: 0 make_network_request 0.000s id: 0 api: https://example.org/uncached make_network_request 0.000s id: 3 api: https://example.org/uncached make_network_request 0.000s api: https://example.org/uncached id: 3 parse_network 0.006s parse_network 0.006s parse_network 0.006s parse_network 0.007s read_cache 0.000s id: 0 cached_network_request 0.000s api: https://example.net/cached id: 0 read_cache 0.000s id: 0 read_cache 0.000s id: 0 cached_network_request 0.000s api: https://example.net/cached id: 0 parse_cache 0.004s cached_network_request 0.000s api: https://example.net/cached id: 0 cached_network_request 0.000s id: 0 api: https://example.net/cached read_cache 0.000s id: 1 cached_network_request 0.000s api: https://example.net/cached id: 1 read_cache 0.000s id: 1 read_cache 0.000s id: 1 cached_network_request 0.000s api: https://example.net/cached id: 1 parse_cache 0.004s cached_network_request 0.000s api: https://example.net/cached id: 1 cached_network_request 0.000s api: https://example.net/cached id: 1 read_cache 0.000s id: 2 cached_network_request 0.000s id: 2 api: https://example.net/cached read_cache 0.000s id: 2 read_cache 0.000s id: 2 cached_network_request 0.000s api: https://example.net/cached id: 2 parse_cache 0.002s cached_network_request 0.000s id: 2 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 2 read_cache 0.000s id: 3 cached_network_request 0.000s id: 3 api: https://example.net/cached read_cache 0.000s id: 3 read_cache 0.000s id: 3 make_network_request 0.000s id: 3 api: https://example.net/cached cached_network_request 0.000s id: 3 api: https://example.net/cached make_network_request 0.000s api: https://example.net/cached id: 3 make_network_request 0.000s api: https://example.net/cached id: 3 cached_network_request 0.000s api: https://example.net/cached id: 3 parse_network 0.005s cached_network_request 0.000s api: https://example.net/cached id: 3 cached_network_request 0.000s id: 3 api: https://example.net/cached read_cache 0.000s id: 0 cached_network_request 0.000s id: 0 api: https://example.net/cached read_cache 0.000s id: 1 cached_network_request 0.000s id: 1 api: https://example.net/cached read_cache 0.000s id: 2 cached_network_request 0.000s api: https://example.net/cached id: 2 read_cache 0.000s id: 0 read_cache 0.000s id: 0 make_network_request 0.000s id: 0 api: https://example.net/cached cached_network_request 0.000s id: 0 api: https://example.net/cached read_cache 0.000s id: 1 read_cache 0.000s id: 1 cached_network_request 0.000s id: 1 api: https://example.net/cached read_cache 0.000s id: 2 read_cache 0.000s id: 2 make_network_request 0.000s id: 2 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 2 parse_cache 0.004s cached_network_request 0.000s id: 1 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 1 read_cache 0.000s id: 3 cached_network_request 0.000s api: https://example.net/cached id: 3 read_cache 0.000s id: 3 read_cache 0.000s id: 3 make_network_request 0.000s id: 3 api: https://example.net/cached cached_network_request 0.000s id: 3 api: https://example.net/cached make_network_request 0.000s id: 2 api: https://example.net/cached make_network_request 0.000s id: 2 api: https://example.net/cached cached_network_request 0.000s id: 2 api: https://example.net/cached make_network_request 0.000s api: https://example.net/cached id: 0 make_network_request 0.000s api: https://example.net/cached id: 0 cached_network_request 0.000s id: 0 api: https://example.net/cached parse_network 0.004s cached_network_request 0.000s id: 2 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 2 parse_network 0.004s cached_network_request 0.000s id: 0 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 0 make_network_request 0.000s api: https://example.net/cached id: 3 make_network_request 0.000s id: 3 api: https://example.net/cached cached_network_request 0.000s api: https://example.net/cached id: 3 parse_network 0.003s cached_network_request 0.000s api: https://example.net/cached id: 3 cached_network_request 0.000s api: https://example.net/cached id: 3 cached_network_request 0.007s id: 1 api: https://example.net/cached make_network_request 0.008s id: 2 api: https://example.net/cached cached_network_request 0.015s api: https://example.net/cached id: 3 parse_cache 0.004s parse_network 0.004s parse_network 0.004s parse_network 0.003s cached_network_request 0.006s api: https://example.net/cached id: 0 parse_network 0.006s parse_network 0.007s read_cache 0.002s id: 0 parse_cache 0.004s read_cache 0.003s id: 1 cached_network_request 0.007s api: https://example.net/cached id: 1 parse_cache 0.004s read_cache 0.003s id: 2 cached_network_request 0.005s id: 2 api: https://example.net/cached parse_cache 0.002s read_cache 0.002s id: 3 cached_network_request 0.014s id: 3 api: https://example.net/cached make_network_request 0.006s id: 3 api: https://example.net/cached make_network_request 0.008s api: https://example.org/uncached id: 3 parse_network 0.006s parse_network 0.006s parse_network 0.006s make_network_request 0.010s id: 2 api: https://example.org/uncached parse_network 0.006s make_network_request 0.007s id: 3 api: https://example.org/uncached make_network_request 0.008s api: https://example.org/uncached id: 0 make_network_request 0.007s id: 1 api: https://example.org/uncached parse_network 0.007s make_network_request 0.006s api: https://example.org/uncached id: 1 parse_network 0.007s make_network_request 0.008s id: 0 api: https://example.org/uncached parse_network 0.005s read_cache 0.002s id: 0 cached_network_request 0.016s id: 0 api: https://example.net/cached read_cache 0.002s id: 1 cached_network_request 0.014s api: https://example.net/cached id: 2 make_network_request 0.009s id: 0 api: https://example.net/cached read_cache 0.002s id: 3 make_network_request 0.009s id: 3 api: https://example.net/cached read_cache 0.002s id: 2 make_network_request 0.008s api: https://example.org/uncached id: 2 tracing-durations-export-0.3.0/examples/uv_1.png000064400000000000000000001106751046102023000200100ustar 00000000000000PNG  IHDRnh"bsBIT|dtEXtSoftwaregnome-screenshot>)tEXtCreation TimeDo 14 Mr 2024 11:52:36 CET1S7S IDATxw|TUJH{)b ( (® " *|" PYTd@H^B̤QGd=sν3|9cczp"q  [(`H@C P-ƍչsgz*ZZne˖]iaÆ,K}o޼Y={Tr2e5ŋ5l0\.xL:U 4PXXJ,GyD;vȱ3g*22R]vWLW [7׫yڿWRԶm[-^zM7޽{;|Zj%KhԩJNN֢EenZyذa4iiӦmQFS͚5rJ͚5KWV&MSի{9mW[7gyFgΜѬY԰aCURE'OVtt"7͛7ӟ'uQOmڴQppnv 2DM4)=m۪A{ug[w߾}=zJ.)Snݺ5o<>|XC q8p*UG'pJJJҊ+Tn]UZ. Q{n}Wv5h UXQEQٲe5b]ᤥרQTpauZj^xA[nEj߾>\K,QZZ7oB S:u_ѣvy۶mC_~s… ;vj׮h+VL=zÇslW$nܴ~IRݺu}իWOOvuiJNNրk֬Y.hn`oկ_l5,,LGֽ(_d$_~9aY׿K.s!IRDDϱڵkvkʕvY.]4x\ǒOkر5k;yi>|xm-%IJ9)۶m$i֭ڲet%JriСvܭ KLLرcGɲ_~Ysф Աc+S,˒ͯ Yfa[Vft_bNW(N8!)%''K"##%IsQ߾}Uxq8\vT6)))P!C[nW4OŊ?{zG>HϷU~}I˯Xq 3))]xU\YCծ]Yƍ'Izg&""BׯW\\wQ -EoU֭5h -ZTJΝ;չsgIoʋ#FĉjԨwrJ q %I:}1O$M2E&LPxxƍUjȐ!x PRR^{5MBBB4eH5i$M4I>\pAׯ;|]*!!A7oְa4i$ 2D:t1Fk֬$mڴIgϞuV"E+hJKK5uTu:2٥&O?^ʕ+uqzWԢE G3f_mۦ U^] R޽Sx ӧ̙3vYDD,XXI_~eY*WZl{NժU=zT7$YƾW_믿qbbb`og}V?Uvm 4Hsu"bYV#N8~[K.U 9qJHHГO>yCmAT*;Kݻw+<<\ݻw$} w|ɲ,}v͛3F6lP֭2eʨ:q@&MnM*S^~eQP!ԩ"""nݺiΝ~WBBBN:)!!Qg4h*V"Elٲ1b$I/,I;v&Dxnz-%˲T~}?^ R-ZᄈתUò,$$$D#FPFXBBBdY~a8kL2PƍzUzu+**J:tЪU.~/&n˕+SNiْzJNr| :ԧ]\\MfO{8qjժI&w>ƍKJOZzƌs,K=zP-4uT 0@}ZlǏ;1B}ݧ 6h޼yڼyZh7JJc566V֭ի_׬Y$I~mIR$U\9O1Ϝ9SwVHH8Ȳ,3fmۦ{WqqqM6sΒXY+..NҚ5k]IIIT6))IO&LPBBƎ^zIIIIܹ-[樗{W?5|pڵK˗/eYjժkGSDZoF=z]crLuΝk$]cٴid|MΙ3gLttYr]6{l# 6,߱{TVH2<|F֬Yc$f͚ӯ_??QVzu#ɜ={֧~$s]w9ʧLb$G}Q>yd#Ɍ1Qb"##M LZZ]k?>oΥw$'=e˔)AAAjԨ$i׮]vu딜M:>, ުrSNnݺI>snuQO###%IsÇ1=&M\v_wZjΝ~[/e{9s U>}t-ZHn[W^J\;{1ObRuOzz/;B=cKDXn]}~g… _~ڸqr)s+ǹsw^*Tȱ/]$͜9S˗/׹sԾ}{>d|5k֔zQ嘸uݒ$c1OYc69nӠAEFFjŊ:t萣μyr+1.\O?q?$k.ҥ4g ԪUKwuK͞=۞WV9|tK.E9O>N111IFߛl\RvZ]p>xI?HIk*Iׯ/\^x={VsM?X<7o.Iu|MlՒkԨQ?O?TÇפITti=Sv3Fɺ뮻4yd-ZH#GTVieѐ!Ctq;wN'Ncկ__.Kʕ+~kR ;v5|?^O= *QFm3`>}ZSLv JFkݒݞvZmwH\Km۶ոq4uT9rDݻRRR4~x{;Ə-ZH .l^{5oBBB"##իM*U偁fv{5k1SfMdʔ)cƎkΟ?o\.d~8M֭Mhh4]t1;vvܹI&&44ԄFs:L>ԯ_߄(ӤI3c MfUf .lJ.m)~իIG5QQQ&$$4k|ٶ;[MLLIKK[桇2QQQ&44|'pŽsXxq?;wΌ5TVHӮ];78ƽkȏ7R; KbߤT5jPbbN>ܜ;wNe˖c=7|*F\Ϥ`]Wn?gk%zeOXo='x*E|^ ?Xeuܹ_S_}?.;5j:v쨚5k^ynk׮\իWOU<:55U?s*WdUVMΝ_m66/^Բe~Ӛ5kTZ+:kJ^<?~NnݺO>[ժUjݺn5kL/nk.+-07{@ P-0$n!q  [(`H@C P-0$n!q  [(`rMٳG>ʖ-0-[V/5 OVժUu+55z1q{EuQ˗/W||} G1& jРg'kwܩŋ+<<yS-tY_R3gThh|+*0{$GbŊW7VBB|wW "7[%޽[z%I&LPxx5uT >\eɲ,=3:~ڵk`裏>:}^|EU^]Rj*ǘ]v3f֭[-Z($$DJuE%%%cǎЭު}رc;wT^TxqZjzW4oۆ ԺukL2߿N8aן6m5j[JBBBdY~a9rDvǎԩSUdIֱck?^z QTT:u꤄GkРAX)ejĈJKK5p,XH29dL֭Mͫjy#ԩScŋMMppݴkH2;v릤$SR%#lڴ$%%cIKK3?dƏo5kf&Ol*T`$;8 'H2֭[ҥKMŊMHHٰa=N͚5M&MMZZyW$3mڴKZ3W%'n1&&&.\<cΝ;gWnOn1fF1b]JJ4*T0iiivmی$cY駟;w˲$/^4J2̡C9uc'OWZ(]d~g$[o5ÇT^H2gϞ{ҤIF01Ƙ1cƘ;d^LLOvǎr;Νk$]cٴid|MΙ3gLttYrew,KϟWϞ=%I ֭[#H,X Iԩ]DDڴi]v)>>џ$+WNv]^B,YRԲeK<00P 6$ڵ+g֝w(oԨ$9sVLկ_. 'OuUʕ%IÆ ӢES.ۭ:9sÒҷm8z4iX\[;o۶MTR%cd-[|/_ާH"ϟGuq%''zfΜ)IJNNio.sijw7Gܵkזwҥշo_ǫr:t%%\[?*,RTTcN~gV'O+TP] qVESX|v)n|[o{9reL;SGָqjРAz7\\u\&S%IgϞ8s挣εɓRuA{Brݲ, 8PO4/wf5j(UTIcƌѠAh"M>]mڴI6ٱۗW֯_xܰaCIѣ~޽[RzvժUviii~۹sDzg[Ooi~~W^ny9asR9ٓ47nLe<:ƽsow]q|dr\e9q`dei7^>~=/171gNe&ciN>dzf}l˲}yY)}kgD,8>Ϋl5s ;3mQ瘖nqeN~ϋd2"˶]u=su?&W[gʲ26dys{^,:F.k,e'^k5v2䙶e"KVFe_3vm:ҽɻ寭׺xbqKʘW)GǾc9uOlsWs8}n\.+s ɧwy\~nd{sd<ߔq}Y^1y^oYyۃyzxZu!eyVr;uL^+3̺ƞ^U/]^oyC%ة9)m˫g}2WX,ɘw鿣.5Ni6=Oce}l<紌&doy3Nz;}<9?d4'>S.#1Y׎'^Nun9wP(|e׍bYLjF%wxa<1\ngF\?2nﵖN˼@_#+ H/Se\1[}zIU{95\\OO&6_Z:NܲU0$n!q  [(`H@C P%n˕+'˲dY6lx5K޽[ .V nиqk9ܐ,cV8p@JR ;5kU#xyԢE ,YREQVvZz'OVƍ05nXӧOSjղ,KKVrr${r\,K ָqv˖-w߭"E(44TwyΝ@mv֭kӦMڽ{~aGaÆ_~jժ~'m޼YӧFk7oV%Im۶oEJ>?^|r 2DxbmVaaahu]&M"ۭwŋԩ?5~sN}ǒ۷jժjڴcccl2%&&J**aŊjٲZl˗;7n.\e˖I1ZN8;w*<V^=zvZj]dzBHHOPIɓ'H޽%1c =;k=S6lp|%&&*))Iv[(0UTѧ~͛7k̘1>}^%%%DvBٳ>mϜ9#IkNj׮ujݺuJJJRժU5sLzRSSsfm $VZ6mF}5kJJ<֨Q#x?u)iQvڒu_dݻ$~`I<-Zȧ_|!I2c߬z)˲pBM>]>\.gumݦD-YqĉzwMܖ(QBӦMӲe˔_~Eʔ)mJʗ/#Gjʕzg۷O/.]wJի%I;wѣG},[Zj5kh۷&Nu]o/^w}WwqwY כe_oF*Y *[̙35~xOҷP˲,IRrgM@@막':?IM4ʕ+ |z嗵|r9sF˗W>}+ VMeS;;իF>+;&nG[%P-0$n!q  [(`H@C P-0$n ?fk5k=vNq\zp:\ͥ[\21r73b7VNqgzr>cNٍ,s[lyY:\1P-0$n!q  [(`H@C J>r\,KeԩSW*.\ak׮UTTz \\Vo+͛7+99YVޡEvPT)-[VW"\%ݻwղe \\v7=; 5}^5>nc䲬k2$Y`;sc}NeLעocey-j<.ͻ1]=3bzuʮ]^c_vex/}nAnm>yr:'nYISЀlU[%c4aկ__*YڶmoӧOo߾RbԡCmٲo]vWҥO{[jܸƍk>.\cǪvڊVbԣG>|QԩS6l*Vh?QiIir?AXѢErJetwH" ՝wީs-Z Yu+""B8p]wݺuzULEDDvښ1c}iӦvm۶ӧ/z VTT:t೭B~ƌ 6u S2eԿ8q"܈,׵gfcy\}Sz͐tzfms}xץoͮ1XSܹޥOSvg-\[^#./yr:=7ϦX/r-`С^zi߾}Zh6nܨ;l'xB%JG_Zhcǎ9޽[7iӦi޽5k/_ƍ~s6lVZ駟~͛>}hȑO?ƎYfرc7oϟÇu?A&L/M6_̙3բE<ZժUo>UXQ4zh}嗒۷O]tQɒ%k.5mTxbmVaaahu]&M$uIǏWZuVJNN믿gױcǴclRJuAUTI}ѯ*IZrLw{SÇ׮]|rYVZ9qqq6mO>ݻkĉU&M޽{id#))XeGM@@ٴi]Vzu#Ɍ9Qo߾F裏}1̒%K˗/7̣>j/Ʋ,ӬY3nrLRR]VxqӱcGG-[ۏNj$1c86̿okѣGI_9ֹsgK/ُn\)V9y]~`J(an]ޫW/#ԩS\xcL.]̐!C1Ƽ{FYv/45j0vٶmی$oF1b<%%DFF *4~$[o5Çn6sI3sl\ wU8j;xv=}N99J<.ucNy)s[lY˹./Gֲĝn1?9c/B?}vժU˧M==wر;Q *44Tw}~- vK.\(c:u3^Νv駟e˗駟/X@ԡCG5$-rӫW/I'|(?uꔾ.}vjJl,YRe˖աCźs Lߒx޼y׿eW&M .Hڷo-[ǼʳY7""BmڴѮ]O2eT~}<((^]v @d$˖-(Xʗ/MLL'qx9:qʕ+&ebŊ:u#Iڶm$RJ>U\Y1B'NPFԽ{wǾ[nX  IJNN;TTIM6Ubb֭[gϟ?_ 4R/pݻwg;wwx@k|jժ魷RJJJcz|ȿl sg;Bs?6$$qx'O̵~ֺ֭sYfj߾8`a맟~RRRl~xv={]6c ݶc1 $vm>r-~ ?CСC_ *dmV]_B mH"+gϞ{̙3z9Z#66V Zd5kZ{,Yb0j(-^Xg> 6q{+ @3fSk:> Y?C:s挾[G?cǎ0}E_رJwiFqqqv˲T^=YeڳgOI{gԩS/õ<%,\PӧO&AԩnMvĉzweY^s=(|hXpp]9YE.]$g}O>N111%cȟl*TЋ/={y0a̙^`۷ 5kkƍkG 0@˖-ӱc?_~kf-_F+WӞ={o>KZtl5"EW^ە˗kԩܹ$I]v߯+VC7oϟ'xBv< Ӕ)StQ7qD{z7xb;o>ޙ3gaÆ׶D5a={VǎӐ!Ch'%iժU͛7vy^Զm[7NSNՑ#G޽{+%%EǏwlI'&&V֯_iӦƍ0j7nl,X`1f7T IDATrIWn춳?{wfEq}[sfaD(`p1"5 x׸cԨp1*q!j5\͸6 ˰̜zfaPF}yfNO-[K,[toߥK_PP;uO9֙+_%%%|w7{lo~ӗ-[Ϗ?ޗWQQ᯾j߷o__XX۶mG͛"iИ1c^ǎ]ζ?Guϯ+_ۦј4Jo/5kc˫_;y6ҫ/Ou姮[S~{`{`4 4Tuk,m܊43>s!ClҥKYzuaZlI>}>6nEDDDDDDDDD=*ADDDDDDDDDƭH3[fF""""""""""͌6nEDDDDDDDDDm܊43ڸifq+"""""""""hVDdxww\C\~w@v=TMDDD> ڸfɻݝIfwg@DDDDD3L""""""""""͌6nEDDDDDDDDDm܊43ڸifq+"""""""""hVDDDDDDDDDƭH3[fsq;~x 0`aŻ;Ko}[u޻;;u?Aii)wΊgnٸ}ܹ.5k/F|MxwwVDDDDDDDDD> vGW^y%r7wYՋ+V8A,4褓Nbڵ|_YLT7n׭[Ǎ7w9]1f5,))'?ΆH 70_+ʶ" C[ ր$EON8o0x  ֘`BXCdƆ4=mcqa0DƐwc=BZcxBtfϾc,xGH'IS ˼U|My8<>DQO Xw&CY{x" yXcscO6HkB4 `1'CW3 %1&Գk:"]& LJB|K7Trp"Vn( ѦȲx}%"l2iy$ז|}C? M50؅qs.CJ>TuP׸dNHڳ1bl6͡U$} 2YIjƙthN#.'.pK %0a! O1Kx:& dnL }%Qk%eFGc6ĕ)b1xOha{ޑUܚ̋AdMV*#JZƦ%T,^&qP`MΤ-<9Vm}&uY1q 6 1&Ey `I1Y2֐OƼ8v0u5X2I Um"m/>\9Ƹp!O>بj=RЪ,JhQ`xcVޭsaIb±GqDe1:xߕ/0_}M> 7D&ֵy+d("ߑ kxB CH?'ŮlDn ᒾ55 qBպ56aOHQXYW+c"4Ju$X>ޜ{mZ$vDk-Y_M',C98bgXW"bs>\Ooܔ=};LœUϤQOBX{|1fX">s~ }{ʶ:z)`Ȧ#ځdMƞa<1;Lh }$qVMU {GaZt55kp9гgOJJJ6l3g{-ZK/nܸ>ҵkW:,VZ8p W\q]w]օ 6)K,{{-[g{)))QF1wcbA֭[Ȟ{I˖-9Cyꩧv(oaJ~_ѽ{wZlɠAxgelْRƎKYY]u_ٵ1իWs)о}{JKK9cxc̘1tԉ-Z~q~zk]wT7&s;ݺuuߟ￿<|ԻqGqW]u6mbĈx9cXv-qs1pmqWop50yd=P6n w=3x},ZEѫWFgxݺu|_gʔ)~{l1ѣGaÆ:ϹK8s9c?>SN7CeL<?VX9眃1okwy>^xqeee 0-[p9d&Nȥ^… 5jJgϞQG?O&LwIii)wZuk{(x9SӧO<)//%K4hofbҥuQeܸqKL4zhl}ӥK{=V\IϞ=9SYdICDDDDDDDDG%<5 .{ゥ䡇 >ydyZN;4?nVƍG=ԩڵwMwG}Ygo~>Z?:F ۷/_='x"&L`ʔ)t֍ݻ;ЦMn,޽{3rH~_e_֔KJJ믿ի_ݲeK~s}|%;~UWQQQI3f 'p{./"5WNXx1?я 8Yb<O<'pB^JYY=̎r-,׾:뮱{'ؼy3gqEEE7w}vIw.Z ۠ѣm۶婧"exG9rd8 /2ntI5hѢVӧktk6>6m3PQQSqwq5|Av9SO=Eaa!|k]'4=3f̠UV 2$;ޱcG^xZhڶm MG[o'""""""""yV;w|Ÿo޼޻FCq<ÂvIf!=Pnbu;Sg~_V|m>,XKҳggϞ5^[lɎ-_͛7ӣGZlY#|>}wGZ[>Ϛ5k(++o߾6f Vg~v4߿?5k>gumڴiT:"""""""""un>0e>lz~;***83kM-3yG;cӦMP߱4o'N[VzyѸs\ǫkuVOSCEϺu꼫-[/rW׿fĉ~;gEQ1sL СC1ư{s-py~QV6mv蹵MQRRA~yrMڵkkş~[yq7FV]Ǫ瓨nw`7USצM&N_~;&NgA.]1bADDDDDDDD䳠g`РAfڵ̛7$߿?sn= {5;jCi|4q0dȐ)}eYz+ιs=,^8{ޭ޽TGy$SNq'`Ÿp Cr-Xux=}t6móG45mf[(8ٴiSi_RMѣcpӦMu^󛒿_\tE5?蠃a!""""""""y94h#.qw]?xyFԩS6mgq??geڴiK,_ }t֍o/Gyn.ѣG+dvʵ^KYYÇfƌ\~vau>o6s2m4nf;CwYtûʕ+8׶,XPcu„ tܙ.?z;Ç~x7kl76;v^ncͬY .Ǝ[g^EDDDDDDDD>Oݸs 'g̙3W_}~ &pg?ej/^رc93Yr%O?4:uu֍?Oq <^{n׮=Gr 3g`O~ 2e tI<u]9VL:r:,.b̙36lXOڵ㭷իWt?#ڷo7ʬYׯ/8묳(++㮻GTTT^{q6}7۔=蠃j}i(--etܙW_}SO=~+_~dӦMtܙSO=+V{r6)ƍK.aĉoߞrJ}Y\0~ۿg'l~;>{e'1>'x" ,`ݺuٗV}^׏~"xCM`c xc $ `01#Z<L^O<ۃǧp z18bއg{5IZicUy̦KU1*ghSdY!ƑđF<Ƅ{I|.&n[kklRfU<`=8Ώ,kc >)x7M$miL$]U!Ő"kBr=`P΃ޓ$N{07I'&/IE:%yJ4ɯƃ3&.4>p:Uݚ$ i:}( BJ󔵽>ҘuǛVMr2Yk^&j"k]wBx| 6*'ikrO-\cIOIirZ[wXveU2F/,$& pI{0K>j4/baO[u X 垖|!`M=Q鵥]fm9 %m0&'cI&᤭ZB|2xIۅY3皬m 'FIv`1!}Kh T. O(-d0ቱY18$-$כ塮C:G֘Iݦ&\Bh,۔jT5dp.{\R`mڥsxڥnZ}ҹڐb66դK^icƘ ĕ@% :Ė^oi*ٜ=qҔ{α"f͖8[TaIKzB8볕oXT{kIØh6WfX!%=*BtkѺвfK̊Mt\WiQI,qΥtHע~:s7(qeLNɵjcNJXkIFؤو9"Kv{1aV}J+PbY6Ӷ jZ՟SaO{gRɺ0g6QU6>\n0UAR.}Ks6NP`'$\BOM&I6i{"C9>PpM$Bkն%+'X]$P%u`¸gd}"}x}|zfbBuDbOl2&|yJnI1еe[bϚ-1&#[qm_ȿ6XQ6]M>>[gc8\ m¤-Usw ISLa{GYecsvܹr!u]'Ǐw߭ϲtvÆ jjwgGDDDDDDDDD|Tƍ(++ {ңG]|0S[l%5U#GDDDDDDDDDdw(!Cӧz+Æ cРAxYx1&Mfyz1ym7\QQ.I>}}g'H]|Tկ~̙3Y|9ZtСCWO;'|5=_s%"""""""""_Dn܊Q3nEDә= IDATDDDDDDDDdƭH3[fF""""""""""͌6nEDDDDDDDDDm܊43ڸifq+"""""""""hVDDDDDDDDDƭH3[fF""""""""""͌6nEDDDDDDDDDm܊43ڸifq+"""""""""4ۍ?R1cOV^^N>}8;;"""""""""Ҁfq^{f.ݝυիWkQ^^^㽥Krwȶ-1l|.у￟iӦѶms=ڸiF vw3vnc=5\ÀvSD>9^1ZZDDDDDDDdW[)chn֭nc}ߟny+W3Ϥw޴lْ3g֙{ǘ1cԩ-Z` /d f_r(W_}{ӵkWo׊'ӢE w_&MUW]Ev>|x~;RFܹsƎC׮]y7ӱcG8Ύ92 ߢE }gT՟}ٌ=rfϞۛtM"""""""""|9~Fscݺu 43f0aJJJ7n7pCJnN;4VX믿N˖-9r$ͫvɒ% 47xYftR:(~2nܸ33`5{ꩧ2k,N֬YìY={67زeKx~_qEѹsgƏ~g~ի\r {.{,gԩz,XEL<_p,[yff̘3<[XX?O~4Z-ZDy饗8Yh-b̘1}mqq1|0fbƌ;SO=3gҢE Ə͛so|s=֭[ӵkWN;4sL>F^z)eee]۷K/ N޽yׯ_9NhZh9r$˖-cY8=7t۷ǣ>JϞ=׿UW]1b&Lo߾y\l޼ &43gy&֮][?Nii)SN^AA#FW^ٱw޽ԩSc;vd޽{ӻwoڴiӤ]/'3aڪ:t#`Æ 56Cot!;nDZ*;gƌjՊ!Cd;v /[;I7p>(?xtR?я\X]yX|9~e{b ~mrӧO9ȑ#kě~q/\+ ԩ|0K./?w<#8|I?4Si}H֨/'ݻw}?p֬YM7wV^Mqq17/5kPVVF߾}km&6<s=k={⦛nbl޼b/_^+fK;v[p!?/~Q+Ͳ:Ґ#G2g|I͛yǘ8q"K.ncÆ <쳵6["""""""""zmڴ r W_O~x ,XZuТEFggܸqDQo~>ZaϟϠAx';Yp!O38^{ӧyx8cҥKvÆ L8hѢ&=6޽;k, <K_G0sLXQM)oli>tfѢEr)xY8}sgn{kC}]z)L 7~#xan6me]!`nF,Yرc1ХK=\Zj@.|:3cڴiY}_k׮<#|̘1c=M)oli-XȂKпxbѓN:sWУG|Aδ{Exb֯_o͍7H۶mkK3gNZ>h֯_ܹsy9,o,]s2sL;:Z=VDDDDDDDDi)Se˖sѺukկн{wVXs}e1=J?H{ϴijEwsΩq7p]yXz5=X<@2>}{4x'p|x= s6GŘ1cXlgЎ3_}zwgß/@ӯUEDDDDDDDv7]A= w:rtB͉?c9{:uN{eժU)"+lA [M[3n nLrαyf|… vʔ)r)TTTdsգ***xypѾ}{<@}{;"""""""""ȧg܊ȧO""""""""""͌6nEDDDDDDDDDm܊43ڸifq+"""""""""hVDDDDDDDDD)xqJWB2h-17཯3k.껖x^c$̄կz{乱q6¤u9Mv* cwi>we,_~4ƏO2Ź3mi˞dvk>Z2̼Y#]>ԿټH?g5ĺx0;fGߙ)W!^M>o{6e`&絹=VD> `\[7"[6Q\zwcWiWv27}Qۈ:ޘc]5vnp{e5,>4wg: }H|Ɲ/F||~{m=f ʥk+\cݘtJJ>5-;XP5TZ6u.=n]4\wum[ ]?!Ә϶756OTMr=Z*eK1qR6&ƄٝWu+;_SkkGa\ )x{ kvm܊43ڸifq+"""""""""hVDDDDDDDDDƭH3[fF""""""""""͌6nEDDDDDDDDDm܊43ڸOҥK;ww6DDDDDDDDD>3q+{G"""""""""M`~wgB>nc=2`^~ݝ%תciCyk 90`8ߖ@y1=#}q>x 9k]7k{c0b60I1ք37C:[YAxBΓLȇ`CӘ/>΅k ](#0x 9 :D=QR!=#'CZlR'p( eאio=>I?X<Bԍ1!}Ks2_ԅ!\h#EȘ:0І"޳Iއ@쉁\dk`ip9SUKOqS qx7>;1"䝧Z\2]">i ƀ!B?YCA 8 ='g1p oz .gB=&">=I 9O2 ڲr K۹œZr:X^w]ZHU.ii^J`C_ >{bQhiqc- w1cL=PxpI[ ZǞ M(J)8dHcM:O1 &m #4iC yCduZ<&mņ1!Sc>ЛI#t7&[UUaCqElI煤O֟YCօ]xqVjSsa ;Ak c'ֹ1Z# m9coC[t`l+,^] JtZ$}̇~q sWL҆Bdܲ߅>ք5iwy6$5Pu·6ZtLcӌWnujW@V*c(L֙V Rć+Pǁ8L9c!'Üd\AO7=- l6ɹƘ%}0#.5`>t-_a4p|K" d5|L|2ׄk<_H#uV8O ١EĜ[;s5lІ4NiٜSj3$s|hG!?a. 灱5kȇ:wztIpIYk/`L29 "p6, "u\Eɹ0Wq]v%{E}{r\Joh1e^msTCrS, ,e[b,lx֚JZr&+*]&<EsU\D\g M(mRǹj֬]6k$kJgBpօ\^5Ea&ˠȂK>TЮqk#*bOQdyޑ,6ۮĤ m?VӦ Γǒ87|F0U{(.#ul8gq'cSIdLڴ*b[| d}\֬.d}úɑGIvh׮[na=oRRRBii)Fbܹ5TTTpuѿڷoO8㏛f{V^M֭2Yf s;wo|Y& 0fsO w}0a|Vwq nݺqW{chݺ5?GuTAz;cƌZi슲<=z4̞=;Koot9|չq{ynݺlꫯfƌL0;ƍ 7ܐK/1i$pYgq0|pz-~,;0fΜY#/>(G}4:u'o\G}YK.s=c=3uT|M=P,X;̚5k:u*ӦM/nRС֭O`q\s5tܙz~π2ds^c\vez5og 믿ٳg__,]s)O2mڴZy;#XnrHyUeyK/ph"-ZĘ1c]"""""""""_DQ\\̫ʬY3gֆ}ޑ#GңGƏCJJJ׾7iƾKYYYy} /p%d:w̃>H9sxɟ?m۶y睴h'Kݨ|]w#F`„ ۗ믿O< &0ey 86lx`v ZʤI/ɼyիV#/gժUL>~\{̘1x_ߟ *9ADqqqC[rs̩q|Weǎ?(**wM*?/,}NwlݤGq6l`ٵqI'ͼW^y:}FV֭9#Xx1*?dذa٦-QG;OOs#Gֈs5жm[^x͛{g92L^Ό3h۶-*n߾}i[_̙CYYC6m(ʻz=UOURDDDDDDDDDT]wJZ/ w[={^[oՈ&%@iiiv'… ~FV, {%n:I')u T|pW\ڵk)++VZ1yOˣӧNv}YH6mT=أ޸6n ^nzW Xn]IO8o}[ޯ~駟^{E]Ĕ)S2e G}4ӟܹs4U}eO>u~I;KKKk؎e)"""""""""Mר۵k{M(ꍫUVl޼6mx/ݘLä率ۋ359Gy$s'+`̙|߮Ƅi$ t{˖-ޫ+?,EDDDDDDDDiE{G4dޫ^(~U7gg~V]^|EVZg>y̞=+V4:̮ҹsg:teX|yaӲ}:),,>ulWeC~*0IDAT5jvʔ)5\l=[Zιu 'z,@yy9O=]v;kՋٳg3w׮],Bz!V\Y#܂ _ ;z8^x}c CQQQ4EcO[nq{ׇrm۶6LpSN3Rڶm|ex駳S,5jG9ɓ's}1rH<]vYgզxo6/Įbo>nqx^+΋(CpH h&A$((Qȃ>$EdD2ː8#+dƤ;>ٗ4k,}/~zٳ'aPvŊ;А^x-_>K..I5gwy?`ڲeV\UV3E]ޫ_[ /Ԛ5kh"+Zdx@7oV__>r-ڶm^{5]wunݪs .POO.]_6m1w^_$Ѳe~r9s;CnI+Vz!]VZd/_K/Twq϶gу>Jwu^}U=Zx$'ivo/ֽޫ뭷ޚp?'#'#K.K/[;4￯J/'9@*]w4ԉ{Tܥ Oɂ1IP\@,},ylsw(˨VΥѥ"j鸫A w\"c,jBe,:s *!9'22Mo@)ݒx]!Q :|hT{V_u5s=7rPHH1)Ra[zvX~FH}4&?3֮RG\\AfwiPY<ױJbކNΜ\*W{2VqPtBiS*-.ȱg&t\0;nM EWSM1{լ6 5I{ jML9rt2Si-vbCԉVǏY9˴zPO[wÝ(?HuKmi@(R-PO3tʴև6"(U[P={BQ=%(k&rDާ1n`5mjtӞq=s҇5bt=Vk^B_~{qN߄U͜j+foz3fy/9wc.S)6j,YZۦ6|Ҽu]'׮s}9NLHzQϳzƏsNevԌvмsL\9uי7yG9fqs?7ռ|9QGU1/=޽;Uo۶m']SvoG$I#5~Ok_' ~}:l;}p8n0%0T:G??{ȷ8a6?.ْC}vI'|Sܹsg:fG}$I588x؇Z-n޼Y-$'/={5Qҹ;ͨ=Szk.IpӣuiѢE:Ƹop7khh(BGѶV\o氱h La [h 0na(@Pp C'p" fM.m&5]ށᨙ]AcJizk}H4g_ctENkwid7h(ukI3~oKh>bjp M;_hMRk& 8i p C-4 [h 0na(@Pp C-4Ov#Zl?Οy[V 0na(@Pp C-4 [h 0na(@P1wn`pش_vOv3-4 [h 0na(@Pp C-4 [h 0>ٍ0n(v_/?s[pd-4 [h 0na(@Pp C-4 [hswFF[h 0na(@P?'rI=IENDB`tracing-durations-export-0.3.0/examples/uv_2.png000064400000000000000000001261301046102023000200020ustar 00000000000000PNG  IHDRh:媂sBIT|dtEXtSoftwaregnome-screenshot>)tEXtCreation TimeMo 18 Mr 2024 13:44:53 CET yE IDATxy|SUMJY *" 0((_d:388.0 ٕe({&G&M[z>}М{9pcc+*%$h JH@(!A QB-D ZWkתK.*S/VZiѢE +7|:\rW͚5SO_ /Ȳ?|Mc:tHի+>>^JRӦM5ydc"eYJ,vҥK^KVRRjԨzJB ZWիWiӦJOO׿oXBʕSv`ha[{n-\P3}OСC2d+v~͛Zj9ڴi-L>X&2\ƚ7oTm޼YիW$:uJ*UR\\nݪ(G ^K/ӧ[nEiΜ9;Ν;SϞ=5JrZjXNR||6l˗7nTڵcǎm?AիWرco]}֮]믿.o>S&'h\w믷:h׮]t 4HUTQbTbE >\'pٸ;ԬY3խ[Q^JIJII g|~m򫮺JTP>-]TCՉ'ٹs$RJ5jH2223gh̘1SJ(%K{:p@s"A W W$]$׫mjʕZl2224`5JӧOtAp_[]wu={HnFG/˟x c=(/$I j*RH* r~wUpa5n.{4fM>]G}ٳgkذa ZW[Jʕ+-PeIҦMqFuUKҐ!CTD ǧo@Zx:w:uGP޽U\9(QBuհa鋸Ξ=3fC=_~9ΓO>;m۶1b*NnذAfdܹsդI;۪U+5iD7pCƋXG$-Z4d[,_$I3gT޽UT)(Z,ÇWBB~mGٳg5|plR˗׾}4c ꫚9s.]2ekVZ)55U^W={ԓO>XGO?T_}֯_g5jВ%Kt}jѢ6oެjРnRRRSSzjկ__x|ObY%I ]@˫wZbV!Chǎ,V$gɓB mEK/m۪VZjٲy 0@;vЋ/qfΜe˖Vjj֭#Gۏ9~XǏWb/B{Ѱa}E!Çȑ#jذu%K;`$h\%)썿e:4i$[JHHرcUzu }hРAz75j(}uUWG[oe}_׬YԤIuA{w ZWjժIx<@YZ2˲4p@m߾]RRR瞻4p۶m:uѣG{-P۔I:JRٲeumI}qΝ;{iƌ*RGL?^WV>}; ~z!mڴIT~?֭O믿ַ~'|Ro4hXB&LЄ b IRJ[_|vܩJ۷׾}4rH]{ƺl2I[)lݺ.\ QO?=*IڴiN$+VL/n*ǣŋkҥ;obp_oW~A>,Y3goK/f͚9_-[(66V5kԠAԳg(Eqv3:uȑ#5sLܹSer֭8O._\W]u$/ȑ#feff\r_{1m6l ׯ߮ݻweO~駒|H?w}W6lUնm[=3^vӟ+W̙3գG 2D^;%-D Z%$h JH@(!A QB-D Z%$h\c+E; H@(!A QB-D Z%$h JH@䙠z?~6mRJ)))Iɺ馛4v؋ԱcTdIY%˲ԧO7tEJ4k,)R^G;$gh޽:rΝ-[hȐ!ڽ{*11QFuNWZy] … F^yeeeُ=zT7|swDLЦSVԯ_?nIRV]SŊ/ZpF]H͈#~ T=쳎$nRTH7m$IUVȶ^z]X裠9_] .T .x^ZJv(EIg%I=@$a?Ak.%$$[nw}W XY>n״iSG֚5kԪU+-ZT*TPuȑL0AuU"ETB'vGBsNuY*Qm߾=lwyGS\\չsgZQ'==] R*UTX1UXQÇ$իWO/$i̘1yٳ\.,Rui 4HJRѢEլY3?׮]ò,ǧI\\ ڱɲ,{!qn+PըQ\K{ =sY)duQK.u;c҄M^}:~f̘!IzGtqϐ!CBڥjʔ)Sn4~xծ][&LPϞ=Cڍ;VKZlF},KݻwWf4yd 0@7oÇ;>\~;YF}6lؠf͚iڵ|@m۶V\e˖)##C ШQ4}tI7|_]Կ)--MUVWӦMӮ]{j,KF-[}JMM$_^]t$mV˗/JMMU߾}r|rORZZmZZ[oiժU3f^xK.Zh^VVڷo?6lvءŋ˲,hB#s9F+`ܹFy'B :H23fpoٲH2̪UӧO2eIŋevegee:uIwޑ +==ݎcΜ9m$3rHl۶mr:8Κ5H2w}1ƘI^x2:o^Ŋ7|3gq6h@K,֭[^W ,8O[n$m۶Q>w\I YDnZ;vЊ+BcR]m@ $7333@Zj!W~qTTI r N4?q:uH222$I˗W޽b UZUC ĜkrTJeee  c}]ӑ#G㦤8eIy7n ٖcR]mΤhnN<)IJNN)kY`lI:v$/֬YIKKA+I&M[o;VիWu|УG%%%$If~;%$$xܘ|;~$'s{Wͯ@R/'&/ħ(s~XpY y(N`*THժU eY8po߮w}W)))zsϝw3xK{z5{lXSNl $%A \颞 $>O?w'<cKoer<%I V߾}vZ(Q.z! ?B 9+ns IӦMŋ:y!k%߇HԪU낍\)"&h^$-Ps[M$m4h$}wڿG}g_yq͙3DZ>$zvY׮]3gj߾}k׮ռy$X쮻ҡCŋWʕ"|5ܧ_ jƌ׿tIl26kV˖-駟jƌr_i{׮]%Iw8qBbTcR嚠5hɒ%^gΜeeeiŊdZjj$ia;*IW/\}Y:uJvL>@wq6m*IZnN>]-[LԡC1B?͙3GÆ ӄ T|y=#v=Zjٲ&NW-DhRduaeffjZzcW~}\.͞=[K,Qzz=Z9H[.l۶Mٳ5n8=#*TF̀t M4)tM#G*==]vKh =z]v;v&O_U7oVϞ=uQ7Nnۮ_cb0~GSxq#)\yW̊+L\\c[RRYlV<&&L8gϞ&M8|͵^kbccM ̘1cӧ2Lll:ujpCԯ_74֭3Z2&))tl۶-lYfƍxg6lhf͚3uTS~}ShQl7nlLbjԨa .lʗ/o|PfM#8p&99ř&M:vO6eʔ1)))񄭳w^s]wdown>CSpa>,U&==ݱo?{[ff1bQ5III[o5_}c9F񘙶X2K[UV-m޼Y'Np|X^233UbEGE!ogk>@ԃ>x"p!\V >@ePڻwo$EdddhĈԩڋ2eu ر#zՓeY?ҏ?gU*##C5jPffKk.bgjѢEz'g-_\5jԸP.'|Rƍs|r{ч~ҥKժU+]uUjҤ^xխ[R (A W%$h JH@(!A QB-D Z%$h JH@(!A QB-DI ݻwWŊUhQUXQ/% 'NPu 7(+++"&hϞ=N:iZbۧŋw߽T]^ysN駟n:8qG ,cmUԠANn߾]JRBB% rf͚ԩS*R91m4;Hw-Idl*U.nDW׫UV_~WϞ=/@D~`׮]JHHн+Iz뭷M[oUEQJJ{='NS͚5UH%''cǎZtc̻sZr5k8+WNÆ ٳgN:)11QeʔQ޽uСs^۷G*UbccUF ˎ4mۚ5kԪU+-ZT*TPu)S԰aCmڴI'˲t_UbbߡC4yd-[VqqqjӦ:X㘘ywyGS\\չsgZQ'==] R*UTX1UXQÇ95pΝk$'x"d[ZZqݦUV[n_6?d:c1gϞ5M65E1'N4fݺucǎPB>s9}t#t4jL0L4ԫWH2C 17py̴iLΝ$smEFvmJ.mjԨa.\h6nhj$Qwʔ)vl 40o:u[$өS'ѣGMZZk$~zfk1xH2ƍ3M41'N4+W6ٳkvCbg$?lڴ,\TRř5k\{qf߾}_~H2SL95pscRRRL…̓>h1&33ԬYL:cĉ$3|pGG$Srex-[IƲ,?۷o7eI]~YS\9#߿S7vtҥ:uI MYj]~iSL#8pO͚5$sԩ'L`$DO?c=zԩȰ륤$hmf\.С|֬YF1Ƭ_H2]ɓDfɒ%Z#O[eY:}>IR…i&$͝;WԹsgGDnZ;vЊ+IW_u+WVٲe%I͛7cbbtM7IvQ?c(QB7|aÆ C *~vyll] 1Uo IDATjUIСC5|%%% K^W;vw3g|[8x7nX\畠 ÖoٲEt5ׄl $%7nRJ!eŊ$-?}t"ۧÇ+##þngڴivHfff()$po?񏎸ԩ#);˫wZbV!CS"ۧ ,%''vqIOm/I:vXȶB EB[zu͟??l%K(sqUWM /B\yI&oȑ#5vX4h^} >s Z˕k4!!At)'OtԹ;vLժUJ v &{Bݲ, 8P>}KzUpa=bpa\[k$mݺ5d۶m$Ijպ!lٲ*Y~_1.ԧ}s `ʕy%ܷo_]V%JDO$c"nM׮]%)'N_li`Oۜw.Iz7B9r,!pÇO>s۵kWj̙ڷocڵk5oy<Z*l=zPv4vXM|[2yp xBD:ϵ`<,}Fgws92Yc?Ӈd3<-7׃n|\_pYp}GW_Hlrx?|Γ,wF?x\}8%r{j\EJ ]1ρ2PqjYV83|nj>?F@?7'KVp+YVvAqگrJ,!gpX 9.w|y{]< ZgJXn91h>3eb%waA w]` I޳!W9Vb9SJ9Q|}#o~Y1 csOgܜG/$oy;rw[@,7=cr%sb܉ׄg#A}=?&ײs$JSHYeK,1{#x?y  ϱ)V]\s&\s^+wQGw+}^-zQ_O :^F$o)shԏKhW!Y&oǔc]}/W|صs _ we|c٣x\E|UX0. fce%)hF $  aw9e$yN( kG=}ЗqdoQO咠}xN,w|9s{MƗDCJn?c6^ߚ>bC,_%hd'h忎H͚5SٲeUX1hB}H'QFJHHPѢEըQ#M:5_Ԯ]۾7eY*_222$IݺueY*\Ǝk[hڴibŊ)>>^7|f͚1 3t=hJOOk.{nzCU߾}բE ڰaڶm^z3 6[nviϞ=*^$?Ըq$I/%I ,PvThQ}7JMMU%ԭ[7M0V.\oqЭ[7-X@ǏggyF۷o| IںuW[nEm۶բEyfUVMR8Լys5o\/v3vX͛7O-$cTzu9rD۷oWBBw tUWi޽,<[h8[$%%ĉ4i]6j(;9+I͓1F;wiߥKy^͙3' 6mʕ++55U{ql3gc?^nnݪ-ZYI*[*Vk۶my іkSbb|Ar-xul"IkBWZUq<,Kw^#sNYFw}]i&I'|Ϯ]$ɾ-嚠j*yZlw:uhҥvBK;@z)wۀ_wy㓲>Yyfnݺ)&jժiΜ9ڰaFS}JKKSҥSBڞD.38eY7oN_.3뮻Nu͛_89rDr䚠-]LE觟~/ *]vJ*ג%KOk_ /h…z'OJҲe$I۷oCƬXZh˗kƌݻwƏ8uM,X7xC7x~ZT,YۧzJ_}222TlYvmzgUbEGiӦiܸq$n}C,KtWkvۭ}jVƍdɒ\_z^|E-^X'OTJԫW/=3r87vPZn\+[..%$h JH@(!A QB-D Z%$h JH@\0Ƙhx9|\]u_2縑⸜=>+ZdR֧\~w.3~:o?.\yŕWyǑt? E8[?9czͷ?\"osm*ϭ}_5ȫ!X_^S.sϹ)ނWUy/`A9ie~\n[78hŖs [?P/?u.ZG kYq.K}>=1XT^mr~焑dmdo^1#*81r\ \ O^|3qD^:;c's U^,9n[^幵7 8}D!Ҿos!py Vkv~WHrȹ9&vkwGCm?$zϮ!|;s Y WkcZM)[_UlYkNy'NP޽%Kcǎڸqcغ;v<+66V)))۷~'NF)!!AEUF4uԐzgΜј1cTN(QB%KTuGǏkСR=C=GZ4%%%ɲ|u\%IG)))ŋkɒ%vEM6*Vu7k֬Y+66Ve믗1FO<uW\;S*TPbbԩ~-qk.d'Ns=5kH"JNNVǎCnдiSѣGk͚5jժ- *:rH-toUAbs>.؂/ZksHq\r]ɍ b3)1%cğ ~]>+p#-~m5nAHq~sproAϹmD:6ϵp9U[ps˭HkWCص WW|[YHsɫ<\_!?k+GYv-st d{yvnG+?2圫~P91gsۅtȐ!zGգG/?֮]N: &&F>J.~[zf͚СCvRF7hʔ)5}t-^X5Ҟ={}EamVz?ci̘1>}:>Hgְa:G~z-Z~FiӦYfի_~Q*U$I#Gԧ~*Ir_ԵkW-[V;v-"IZ`ڵkEoQjjJ(nݺi„ Ν;ê]6mڤ_]5jN:wyGҶmԼys+WN[nվ}t5רW^ڹs$iɒ%4iR9dee}aÆiǎZx,R-TM2_~֭Əڵkk„ ٳg$4cY4n۬_.YdyG޽{IsH2_||F~Xe4ic6m2iiivYRLN7onƍg?}ڔ)SH2׺´p?V$a>\^xVl9ǽ;S\)u}V,8˥>r[s^7k\ O.xw+=6. k)HsT/x z]_^F\.R y!*ϭ}_5ȫ!2V 6|Ɨs i.b-1kGLbx<9vsf"o5Vy}c/o3c:9aƘs' O>DƘOk֭]vHݻ;>1/ IDATm۶͛xiQYf*QΝ++I7o1ܹsx]t՜9s첤$jvŋcُΝ+Iر JRK=$I~ꫯԻwolݺuںuZh{R-[V+Vt.]eG}|%i„ :s$Cڸq-ks}պukرC+VB _]kߎ;Ž)m IXbEGy*UTRmRRR Ll߾}:r䈮jnG}˲TJ?~\?S.mA.EQb *`4׈1=|m(DE$ł4Jg^g10}gh<6{9S;wv رcD}:uy ())5*乯~֭| qP\\}tؑ~zVXH7o} <!!ugdd}'jݗ\r ={矧k׮L2҄cohEmEDDDDDDDD$ /{JKKK0uzkx8S3g bذa޽;ǟ?dՁs`Yf^{g+B^z5ׯ^zE߬Y駟rmw^?ҡCϜ s /"""""""""\H XUUU핕!7tPV\ɒ%K4h/K.!++Ν;Gnݺ5j.ٳgca׮]sGmZwΝINN(?n`YYYL2m۶q=P[[رcYhQqIsK.|7G\پ};'d1-[xBشiSDY0>S yDze:t(h{ 8Ewy̙3袋 w뮪bɒ%&N… xݿ+"""""""""GOڋ/k/"䋠1q+ >rH*++O?".\r N D3+2d|I eY˲w^q<3/#1cɕ1j N>dz ~%%%#f̘E]Dvv6]v_|1˖-c̝;y1vXn喘ǭ%\Bzz:ӧO :4j{TFţ>… yӧ;w 䫬dqe˖|WL:*[q\c> oGsqf̘Y~=cƌz* G}6-U.1xW駟nMZZ9[oe1[n1/Sc1UV!8Ns뭷ʨ6'N4]v yg7Ν;rK/>f̘A~9Zf~Wc'W~N>5$C{~#i_}~luOc^D%H)xƬ!}ِz? sm'jNշOGzLx]:Q_\G{oX{xZoriHċ;^oCkO+7^]Gwb o/=x}P_!jĨ+D-b1V xmsmQ۞@;a=1ŋƍ+zx'TW}cy1Mt!M9Ӡ6 oW6cx=2FGy_x5+3`YVcWCb18_[4VlN4?_" u1kH_6c=bOCODXͩH Al~VF}q՗u:bmvאzS8Ɗ;>G ؑΝ#x1Qh}m[}ֶXE{hb{c!\W_|+ކW"GWݲiGxƘbݶ@Zc ,+0Z~Bu;J-3$9uGZFƍeYq aȏ͓N.]MOhEDDDDDDDDDq """""""""H@+"""""""""H@+"""""""""H@+"""""""""H@+"""""""""H@+"""""""""H@+"""""""""H@+"""""""""H@+"riD$Qv]cG\)WDDC ""GȠ?ZD~,CJ """"""""""D """"""""""D """"""""""D """"""""""D """"""""""D """"""""""D """"""""""D """"""""""'@;i$\.eaY[nm쐎_vvСÉ/ ;;oC95exZyw}7#77yZd ˗/o0o)..>kPDDDDDDDDDkƨtĉۗGl:uݻZǫ~5vq5C9[hKJJx'x۷Q/߲^xogjj*; +}t^F9ʩX}'5qfg$2xju)2](?P\n Bkt'EՉ5֦w$VﭥOdVOdV>]à)+*⺐8JklZ9l89P%qlϓUC&nvUxd8P卻 }]F7;ol7 {OɿjhiȹXp^nz޳+3xZl-͓DOClȶhzH*o#-/Q2woJ!>5E[+1Au](**bܸqtؑT ŋMZZwuW oyy99餓HJJM6\wuݻ7ww}<#g[AAo۶_4k֌t9V^MRRR}}YzMjj*ٌ1+W3ÁeYvipM7ѢE 9묳Xtd,"55:^ڵkGzz:wߍZG}IOO';;1cP\\};3aÆ~Jrrr .`QߴiGUVѽ{wn6JKK8p``\!H|+V%m۶%33={kEIDDDDDDDD*1/{!CpSYYɰa0ppA^/\pSNcڵ&Ln /d̝;o:5k0sLoNjj*wfܸqXų>?̆ 8O\Q\\L^fܸql۶)Sp]wn:FRv~~>w_5'O^ ;;. %:=6mt~z*tw -Z)۶m׏kײd 9x?~<˗/gQ!6o~3ZnͦMسg;v䪫b۶mq燈ȏIG|,[#F/gvЁ:fϞ}̙,[~.]_30c ƏOiժM6s xڴiر뮻N:_l#<°aØdΝ;3|p5TN9;(;==[nW_}O?=~S[[=z4^z)7p~)!mMtժU+n{nV~_{nz--Zĥ^( һw@O?s63<3%ߢEbر$''0~x6nHӦM-""""""""cڂB@iҤ K.v[o0|2g}v/5*$/&--Ƕc)?,>CjkkP]tQHAs˖-4۶Yt)III\|!⊈vBǡ!cX` 0 ޲eK>^y啈r4&Mx_6.MDDDDDDDD #UUUt!$ Iz}_R\\|T߁oSRRh۶-6l۟'Έb^ओNb͚5ұc#.cǎ!Ձ]vQUUEIOOߥK88OQQt-bvРAQ9.z?ϒ%K曹J W^̙3믿Ν;Zڐg~Qp _`!*++">+۔)S/~=֗mLpex2et:덧 ]ʉJ4t>S&Nw?0e#_DDDDDDDDxstxb ,:Os7aUVV=W!RSSݝ|w|v#)HNDp;EK X;!eee1e1eƎK֭6l """"""""r< Z{~q<ȪU馛" ٳ'+VX'M6m Im;䙩G[03x),,vӮ]Tv"ZnMJJ ۷o.7FXӬY3ٺu+GTFUUUdZhĉYp!o-""""""""r<@;k, PG_Z3`vȶ3fukݧQjС̝;7$}ѢEFKIJJbٳ'dۚ5kxw3k֬ϧQв&1&sΡ28# cY#G>"߼y" ޿!=~!O;4 'DDDDDDDDD~.Ջ{SO=~ѿ wre]_̲e>|8se޼y;[n%q}-9͛ٵkWԅXzڶmSO=w[oc=wȑ#۴i?Lqq1楗^bs=}Qvټy37p㩧ov3q#*իW_Fm[AAAHs=nZ|I|Mn6oNvصkG47ndϞ=x@\֬Y:ydrss۹曙={6ӦMc! ڟ}~mB|lْSRUUEQQz+.1cDUDDDDDDDD(1K/c k֬aŊ||L [ne̘1\{ٳ>VZmۖ_~f͚1dW_}pM6?fȑ:uwǎ d;(.._}r'r 74h]_O;/~7o߿\K*|M~p}?Oee%\uU޽-Zpm5(3aLBNN={dϞ=|G?L-d˖-+ۍe\~YGT~z***c`׮]{,^]vp8hݺ5[n3wzL੧ s/G#F%"""""""""?e1hEDDDDDDDDD:_+""""""""""QiVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDDhVDDDDDDDDD ۷o';;˲,?ҏZEE]tSO4v8"""""""""q@{'RTTmء$߿7W_|k4 IDATQQQ^x"u.XE;k1o<4imƌZi@}ƌ򺦦 CѫWFJz}]yym+""""""""A;uT-))aԩtIУGNߞ={kܹ3ŋG|ӦM=VZFݹ(--)t??m_~%_EvHIIM6\ve_>_WzIZZ]ve4mڔgݻ7dgg3bV\7pcƌ nYmڴaڵW\q-[҆ȟƴiӀ;r^9r$< jȏiDDDDDDD?Zo>Ȃ #VZѧO C?tܙz ۶{=.ro?x!/ ܹs̴͛7s9PTTēO>III;T-**nݺE,4(j )))̘1?dN8F ~ŋݻw# """""""""r|JgY|M +t3N^gƍ,]9scYfy駩o߾qcx'ضmcƌ,Zn 7ĉCeddvM|$N?t7o·~m̛7/ogqmڴ᭷⩧b\x!7EDDDDDDDD1(,,gϞ!5kFvv6[n"@GEh߾=o?֕H 1lݺR֯_O.c yoZZ7n\ݽbؿ? .|_եK5k^z)III̞=={l[f Nc?੧"///?/KwWprr2'|rȾ ;%-"""""""""VB ۷g̜9W_}ÇxIMM`ذadgg3}t^|E̙È#~eYwN<\nvnffϞʹisV\X$1bGfΝ\qgĎ=/L>.I&aÆzŻK=ׯ3ǵ|mڴK/`>lnwzaÆt:5kVc 9rrr9sf lž={m>,I׮]ٰa/2{Xcq,YL8Ѵm$%%<3D_|8pIMM5'x={1ƘO<O_XXhƎkڴicniݺLQQٶmiڴ?/4SQQa:ӢE eV^m1fk׮&))׬Yl޼Ʋ,sM7oTVV;g3`fRSSM͜9sua dm۶mW]uŋh1XҾ~]Am9,NvΜ9\yZhmm-<o6m¶mrrr8So˯QDDDDDDDDD~~I;qA+""""""""""ǎhEDDDDDDDDDhEDDDDDDDDDhEDDDDDDDDDhEDDDDDDDDDhEDDDDDDDDDhEDDDDDDDDD9֌1xkpxjq:T|=r80੭[[kܹgaacp`YVpeE=,,Nk?O K3K"e7t_ DD9Ѻ'Q/6?-о /8.hs$+`qA}ޯ` v\_wxbhEy҆(o~FkP"籄"zG{͡@ۨy=s#qr.o "sp༇dr4BUc^̹i`9#'޹5FY!Xũ#s]"SgIQo]1bD9^#o}a Q >ܧ01cҶ:Pѵa;3lu<7sh9Pvq;O6&oz[uapL3O{r"y׋t6A+",c0 ,]TqxSooxe 7P_fHXeA*ֱ\9܎(Ul.G(s? /7ھ(ϩ8S9X%Q'ڿ!G;Zx8}>&c hewpY?v>Vrx xF\?5~JD8;9_㑉v kc>-(yӂϗm =| 'VL9bnv$LyM 7^ n mz5swn?klMbŞf),결, P1ڜ"+bk˲8rbO ɾkaY h̢\-Y+1mDǰ@-edsq*Lyپ~b h}\t9tن'6:ebw S,vۜ*/i.VS'+mp:,_ǁ>iLuEn Yu6Z9ytB* `Y.o?T|}/o^pzۢquVT٬=PKWצ!R2جSK K 9f:pR7[J]}O:CV^ ˂ChdeᰠK⊼ f+?4k"/i_qbV²Y[T%8mec9*~]NqҩW е@ˢ7=2y@}Gme^kh`{o^8; l†u|02\x ^c{juߔ"Eiu,Hq;(;* .$Yl.~:oגbCQ-5e:-RkB{˽l/0u2S}CױjKI-6xlZ EQ:t쬯klۦ]vXEff&w^~_ti.X ї_=#G@}>l(""""""""Suo$`&O̴iHMMe˲8YxqH,o6>Z믿fڵn~K/M0n /իW3w\[:,֬Yw7#STTܹs7owqGԧy攔p50sLMC=Dnn.K.>|h0`+V7૯bȑ}\uU!>\oG%??ҫW/ǕW^{Ǽy"b2d%%%7jG/}Q/_JAA=:~)sڐB>}/Yd +V>3i$n&RSS83x'xgڵ+ŁW_}O> &rssy7h׮ƍcӦM8r-4i҄^x4?|ݥ[lGaذaL<nݺ裏r3yd̙[oŠAݻ7s SO =&LΝ"""""""""?uqe]XݡC ^QFE?N; -1"L ֭[/Xz5۷ogРAY;իW$?>m3|2&M'jժ@G}7ܠ<)X_ IDATBN̂ hҤ K8'''8++VP\\N3\؂hw>6o-///pWm 6бcLjm.H]֭O={P\\;aJJJ߿?F |?X"yO>Q)..=##3ghѥK#b> XG*+# Yf1*/}Z\]eee?e~_DlkOogΜ9̙3?_~܄4T>ޥK_;p_gggG䉖v$v_Hl - Oٻw/{e:t(޽;+W A@III̞=={[f CwȐ!<IJܺ{3dL27yA1(b0BѠM0cbsFPpEDA$&3q2%3IOo}nGuTLuM_{?d}=yo߯*k6zn̜hDDu^J=o|c,//ǧ>`t}{ö?裏ٳg7O~_VeijOX)lw7u]q}>G?w__뾗FXCŋo[xӛ{̙3Cŏŋvbaa!{'<o|;}{_\p!;#>ƃ>}s=`|.^ZZ__G>7??vUU[F?|$87ŏ؏EDnVeDđ#G{~8>ӧ]zW|۷}[ʯJOT;|˷|KODDDwn;_Xxc׮4 3gQJ=oI/@wfDģ+B<5p8حp֦qזuܹab|$-vR'J)kO_*N}q/Ͻ:?DTU}:n;=7ѩxteW'x8:+/߹_X`8˓xх*>svo8>sriX"JSqbiORţ+ø}(<ҍ ;:gT*"Jv# U;7ןXΏb<(UDUSK7l`W&q4"`gs~u]JQώkc44<܍#sIUUQJ_L"N)+W&VRy~:G8su8Ǫ{/ʰW.@FUUt:q|\N싇c}UH7t̕q%z;^m;qpITØןxvOni?{vg?:R"8ˏ,gSUQUwY*~}GUB7]39j4- 4QO,FUUQwx}$/9~ϙazϭ'qgj>Xws^h2Q)%DX7:~J&8O 3gc)I'QE'bg} _v{)ĕq'.7gq}au}=|ne}sFk7{F%ۉؾwlK..qqmGxnX_Ѵı럙Y8J)ѩb4ƤTo!eqi8i)aKľT`7l=O#˓)AUUȅaq+i q|i!F1O&qd Rm'4KU]Zx-xfuQUq|0N=%"2S8؉ITȅ{hDIWŨTqnu;qN<|a^QTj3N:i/_ h!{*D)7 h=gu\b4-!;+kY5ؚU`PŲLvыwpUU'>hX?xk_{_~ROEDu? ؛n>qp[Ons{QۍƦߎ??z*"?rroq?ñ͗pvoz_җ[p(]hhhhhhhT fhZ>'/4wXZRq+itX=IvpxLzފn3ݶ|t[9iK&P:k>{nܥ[fi{6_wYwٶ*NXxN8N5\͟;93gQyM]3snma-3gQfCϜ4 H"H"H"H"H"H"H"H"H"H"HRRJv''h^|ٶ_݊nbۯ[cVxw';s_mush5oЄ6m&kYq}\mT:7S۾Rͦjg6CMA@ D@ D@ D@ D@ D@ D@ D@ D@ D@ D@ D@ *N#O\6_7{qVfa~5I[YfSk>+{gw_3󶏫MwV&ϒծTMɭήv-@-@-@-@-@-@-@-@-@-@-@R;0|u6֔+-@-@-@-@-@-@-@-@-@-@-@R;06Y6hVgɍj~^TdM͕            IUJ)ٝGzAYoԨ,OݵڼƳ;i8\:lj͛74Zm1 gmkfkl׭j)rfnɘC֞zr hhhhhhhhhhhhT y Z=~5F]5֥I[YfSkĽ mޗMiꌭG֚mѦ;VƾZ۾Rͦj7'~hjIIIIIIIIIIIRJ<-@-jou׭n.MOݵ\㶴kh514m1 5Z5ZjkoK5l$$$$$$$$$$$J)%{@?:k~59Gukfi}wqֵuԚmfn,&5\{wS~mT۾Rͦj7'~hjIIIIIIIIIIIIRJ<-9Zmjw{:Ymu}mzn&yom<#nT3k_罻)m6v]wm{fSޓm?45WZ$Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$U)dw`y`_gQ:0?ujsnUMh7]ga1 gmkfksGS2u=j6U=CSs%H"H"H"H"H"H"H"H"H"H"HRRJv''hh^P{u֨53 Sw6,Ng汍i{6D6^gčj4}}ZjkoK5\ hhhhhhhhhhhT y Z=~5F]k{Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$U)dw`y`잀           IUJ)ٝGH" zA'H"H"H"H"H"H"H"H"H"H"HRRJv''h@?^Pk hhhhhhhhhhhhT y Z9>{Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$U)dw`y`잀           IUJ)ٝGH" zA'H"H"H"H"H"H"H"H"H"H"HRRJv''h@?^Pk hhhhhhhhhhhhT y Z9>{Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$Z$U)dw`y`$$$$$$$$$$$J)%IsD?           IUJ)ٝG zAv7mIIIIIIIIIIIIRJ<-l#H"H"H"H"H"H"H"H"H"H"H"HRRJv''h@?^ `-@-@-@-@-@-@-@-@-@-@-@R;0)tEXtCreation TimeDo 14 Mr 2024 11:52:45 CETk}; IDATxy|TɾA!*"\D[QAźA xi+ފT+j[\\PDRkT)5ʪHAP%+Ifɜd# ۈ1$]>{9e1D -DeH@!q Q-DeH8[N]w8 kZd {j,KׯWٳgW^JNNVftmi֭U}',ԩS{Q۶mݻk̙24x="愎'ؚ5ktEK.tMQAA~/qmӦMoF+;;[۷oT3CЮ }駺˵bŊcr-[v.i&վ}{ 4Hxb :Tt륧m۶JHH8cz~luϖ|=zF-Ϟ=ʣ ?I~>9U{賓D^z4i▏5J9 Y`^~K .Reeeu,R=dY>5 q O+55U#Fl~*((̙3OM61FǏסCTZZ^xAk֬ѝwyrWSΝ;cI_|Qeʕ+5sL͜9S+Wt_|ExZz6lؠO3g5x`^JJ|>Z|z)5J7?1խ[7M4IߔGyD~LvIԨQ#=ڲelҥK5{l]wuP. J<-[&ϧ;O? QW_՟'m޼Yqqqԩƍ#F85]wuZ~[9㖟q0`~mIҁԧOIN;M{WׯWLLvqn2믿~Z*--UuWjjݺuӔ)Sh"߿_۷׽ޫ+wZj|>Zn[oU&LPRRQ[2<* [2$n ʐ(C [2$n ʐ(C [2$n ʐ(spK5GPq Q-DeH@!q Q-DeH@!q Q戉[q /jڴҔ޽{kƌ%B5iDeɲ,yjym{}\b<͟?_ -Zd(1q_Bcǎ]wݥݻw+??_6oެ &hǎ~mի5jԨ p\-q}vhذa^zI)))8Yy>uZvdlRGV~~~̜9SsԲeKM2ŽhoՐ!Cƍ릛nRnnn׿GJLLTFF իWGٵkƍvکQFjժ&M$۶%I=zД)S$I?;'D1BGegϞ*++ӸqԴiS%''kOg};eYwoDM4IKbb,7\%׫G}T-[TjjSso5yduI |zG?&n[n"{GEEE &Ti9sߏ5JÆ />[3gԈ#1cF-)8}'b M>Wв, >\ ٳ5f{袋tС&Mرcujڵza 0@֭c/תUb i̘1zK멧$=Z999YgU_{5m߾]ڽ{ƌ#˲׿UӧO͛uUW)++K_뮓$]~ݾ4rHy<}~ynۜ=Uxzj=z衇뮻NK,tUW&Nm۶iҥ,K_|FӐ}1Q ѷo_IҢEzjS4l0nZߵ~5iDÇGGg}wyG_$iСm[O<{9=@ߩsssꫯִi$I:uO8ߗ8AwV /$mݺ5wߕ*󜚚K/T۶mʕ+W]'nCB.,--uBIJ z4ڴi]+<zoۈgvU'IjѢ\Rgu&Lpܓ]v 1c^z-O'7333궟$m޼YR>^#GԺuԸqc\x7T}bcc# , I.]R]}U|X\rI }ٺKCo^_uۻ7p$iѢEO~T}1qkѲe$I_||>,hʕ>,"ɖ%IڴiS55kָPIIJ͙3GOS__^+b IW_SyM8Q3gT-t=3335}tK.ѬYh"=iӦMeu!^К5ktwSG ,вe˴k.knݪcjzgt=(66VSN͘1cT\\W^y$[#h׮]ھ}`;}jF$oV 4H3fٳ~mڴI#FPAAyy^~}GTc&==Hr_G5+W4̊+L#cbb̬Y~G.ׯ_Ę_L.]L\\iٲyMYYxy#p<'Ñ噰͛>N8~Pyɲ#JKKOvڽ{w%p5ԩSu5רK.e=_?G%i۶mGףGYu@ Yg^{5%%%?V1-ܱc$E$i۵kw|#:8իWkԨQG׈#AD~H}TەoY+%%E)))={&N(˲dY~_СC+Ln_Ś}kL^^[/33Jv֭񘫯:|F1|Fy':676˖-8}TB]Y2r-xmܸQv$wߕ$ 2$]jj.Rm۶M+WOZns9-o۶5k&I袋[m۶z{qƺ "?|IxC(-[gϞny\\[>1޽:,I}ݧE)--Jpk;ϛo}I >۷ocpbU6Ϋ|͒3<ʲP2;;ʲ6mT)kԨ$)33򲲲zD,ٳGR^^^$UiWݺAyjzvoۈv*"-Z;ʕ+uYgi„ Jp8ԫz,K.+**˳$IRaaaew,С-ZTm&M+8&G]yU{<W^yE\py͘1C>ƍ'x☯ cdjJJ$Ľ36uNиj߾IA^oۄb=beiرꫯSO=xM>A18ɣjҥKIҖ-[,ۺu$s35kLM4Ν;k׮2Ʊ;]\ IDAT#VZuĺGu֩qn9SkqI15$ʣ'(33@ WSlᒤg}J_ĭ1F˖-$}|˿ٳGmkq뭷jРA1cfϞkӦM1b 3D<*`ʕM6>k˴.wV6uTy晚>}ƍEiܹK$ت{.$Y&޽{KyڵK۷oL._܍۶j՞={"Kh>}t%h֬YZh|A]|k6m*cƏCT/֬Y;S1X~IOO7W||֭1Ƙ'˲"jdggW髴L:tř4sW_zgLLy:ur7-1b[޷oVVw6cƌ1&&&4ǩ5ݻ׭Ӿ}u1fͪ6~E=tPa;}LjjjD#2dH9ݻwD盾}$h?|3:s5={4&##׼''eL18n뇓K܎3Fe۷jt=*k:~N-НrJ:eH@!q Q-DeH@!q Q&dG%˲ԻF-S7卫1c,)Xn1ܺꩮs1"7F&P&})k(cmaqP,uZ'9I8~1 RB$4W٥b ˊ_|N'OL\b$|%OsI䍯{l#+P"+.m$pDIRjj'I_$+61Q~^(9Čz` OW܁y[T%#Y^9B2|yȲ,%Y2By[^؅ʛF_@fyȊM]]7(@&6}2 ,x~5-y&eʓTRŶNp<("o\"Oiq%Jl%OL|y); >Ydo7mRlbRE$Mn@Fɓ(oJkY%NT 9'쒽8[c-o*\=I=/?fɱK$q/clyS);(o|FE%Il1&{M>_ݲ3o'L9+&s]vZEwC1//\5QI+q8dySd&%˫/)$, +&|?W@d//Sv@1 ˊ)& >.*Obs6*; + Ө WN)'4_1-]+o2ʛ<{Q6jU|M>7V$cµIVR<ĥʊmcϡoȓԝS{<$߁U;c@YxP~jrYބj2J=!هw(&K8J9緒 /VyWI=V㹵}B >WPv_VL|{>Ul*zw$O\#@]"9>EmEv6y.;as?$4IF='Fv6Y1)bS(ج-s9%InnP̡cpD%INĥ9* h~=&߾&7dy֩}᪉J9wJ8Īx2()b{=KRJT崊Kʓиx+/OR*ㅯo᪉J;$%uI oXޤ#qW},Ju) V3dy*Xy:RIn@fyϐa'VEk8R$O{?'&I1MTۓqQΫuY|2sYMT&gχ%O\>s//X 7ĥDEԱJ*s9# nq7*zLVLRE7C1{YW屃,o|EN@Ms ܇ey+טRMdQ~_jRzNiXAkސ1{/:ZG{q-K/lHÊvV0vpˍS[OUhzюadds[Gk0o–"Z>/uWȔ6l<_~F2FN~mL֗enc^cco իMy)oY>^?Իw}7q=]Lp5@N`2\U iN;}?Fv_삍$ih,;KO&P(h{d$زK..v9%{/*YLAc\u5`$.ܬ@\JS_*_{:|%{e.?إT/.'(W+0م[*M_vA@dݒM2Np;=r'tg𛊘KvG#2q[mrJvkΜsXbXu|u늂2Շ%G&P@ކ2cd;$SGN{FYϭ&L†8<'D̕ =ߒ=]&S:%eEpT_ECLC_I& e smUla(Z#8[&t=+ .-do#s.&dWƭ~ gKe ?Tvq{8OXdD, sdNWhQhGƖoO|W78)q.3g*ۺ2c*?q ˮZ8qT^(氟k)=1XՖc);(sA'PZ4s+⇎-DeH@!q Q-DeH@!q Q%n[n-˲dYz}ݾ}.\x>}2Ƙ5ݻռysK+W}dGsέSg}[˲ԢE I &#˲3f,Y.L5RRR.͟?>QonIcǎծ]_k#w}9r.b}Wڰa.r~z 4l0IҠA+==][og$-]TǏ$}4h[YYYjܸ 3g6l& JaÆ>PQQſ ռy$I[lQt*+++_%KhӦMj߾駟ꢋ.E]KF3c -\PK,$cԡC+77W)))n-[iݻw˲?F<* Q iii*..+=cnV.\(c Ru]'q;1m۶wѝw_j˖-ݤ$5kLZ޽{u# Ѫ/Kօ^zKmGټy$3ϬΒ$egg1˲t-q~kot6n(I%}F.՘޽V^믿^+VյkW-_ܭzBbbbIII:2bIg놼꫺# w-hڵM6)''GsNhSwц 4}t͝;WW]urrrt駻 Ւ*m>,IItUݻwתU:^ӳ>Q/_ p  =@>l͙3GSNUAA>cIR.]$?sf;w\`Bw.X@+WTQQ.҈:]v$ZI#FD|0$S IO*׫EUiM-3D|[neYZpΝgx"C֭9mڴI}QIJ|=5 Ԙ=5g-YDmoѴiԲeK 4HԦM=ZlW;vΝ;Ciޕ+I+V$Ulժ.b}z7twT /D 6LO<>=:sΣ8,S{o~_R^^5k+R$}*8wq,r=>j\Px*T^]yBgqJc;-Z^/EZ?_B*m8vEw{8ޮ_stϡ#a&lN_X¯v;u n-+-+ڔ~w@ ض|?ַkcî cW_~>xܾ խ.DZsU1ϝ3HvyaT߂qҲ eJ)=$xLk[cdcv ε;Nh^+ mcti쀻?(_8M1`ۖ X ] v4yNZ#˷ql}en|&'KR=Sއ}JS\^VZh21Ʊ/m%Gxǟ$$n ʐ(C [2$n ʐ(C [2G{xdY,RQQѱ _| s=';GpTۧzJU<8N6lؠ<-_db͛UVڴiӱɰat!]tE';Gpԉ[0$&&{=aQM';z0sʱ :jض8l+QB24h*3׳m,y9XWqIz,ŞS1ۨs7zWmƪw{W;12H^cdyxt,y<1XBsX ^Gk,7BײmB1xmߪ8ʔ,YaLJQ}ۏ%.?=znD<e}4w*|>quU7V&M4|p۷/^QQ>k?[۔DQZZ_ǣtkٲen%K.SF .@w/ZHqq ݻ_JMMUZZƎ]jzlRڵ^}Uw^1hР*Q\\ɓ'SNJHHPFF\ w>}֮]*99Y-[ѣ_p2뇧/G8%mʩ:0 ]O}Ӷj؜ygY {*?VOF 'i|ny(Ѐ6N:Vg3 /+6]V ~o*W9!}uLXP)1m;u;vXǩKX#z<u+SyYOy^{|>َϮzO~eȖG<_َLy<*@lsS6hy<Uj+1㊈5|^&l^<ޘ$aEϱe&n8xmr#5P06.cB ۰׭|ݾ+5aum'xN mJZ 8!]XW},I^m|eQy04 >n$|M:K '|?_(^)OUO`\՜qˍ6gϊ9mGc8PRےֳb?`啗l'VsU7Iv7V~9&$ޔ'm1JGcm? ރmSM?Xx~^,aq;>>3-^E`ԊRb(u)V}7Rj [. +`EP0$cf3$̄-e̽|3sMVx9Xvҹwg~C΍D'2ۛ'ω /ClxRs0 IDATn:N=T̙~3X`/y :I&0p@vqʕ+y)//g„ A|Io83h۶-k׮;2b֯_|ɓӶ4M=\|Anf֭[… 1 3<3e~L:-C婧cҤI ><zš5kxG΍7@Ϟ=⋙4iRo سgON.ʘ2e :#Gtӎ;m۶1m4@Vx83[ µkЧO|A &Gq=0b:ukF>}ѣgu}OtN6Eq}qeM؏?)S0z:#G2vXp(W]u͚5b(..f4jt{~;+0(**OdŊL<> ǻGAy饗9OGf͚5nZ6ŋs뭷rЦM^z%ڷoϨQXv[޽{+?3N:$ v#8z۷ӢEODDDDDDDDDj8SOoF֮]qW#ϯ~lj+L8ӟ4%駟Ny?iz7p֨oРAضͫkڴ)/?w-\k}k0`zP3l0^y啔UUU?q7/_ڵk93M[moߞ[WA9s&_L4H$@ꫯ˩.qƜ}٬[O>F9~i ߺu%"""""""""u6yؾ}Gu:tH]v)P=VZZΝ;9#RbGuUUUlܸ;֨裏HΝ;իCMlʕ+rFFHu^x!j/Ҕ.]ʪUXz5ݻwQaޢ"}ƌ֭[ӟđGY=m˶e3n6i /T`0@ %]]髧Mׯ}ϧO>̛7 .F M4SN5m6v :/8l޼%Kpmiiԩ{dկNN֤I&N7nH#G2w]}֍Ν;oVئMٰai<8~--=X %e%nеkW֭[8sXhcɒ%UݺuHjٲ%s%%%,YiӦ1h 7nno ?Ýwɜ9sxw?}֍zyjTYI B o2?| .ٳkӍbW},^Mc=z0 JK/)S˵Kx7xk&㏧{ZpMعs'>:%qxXp!]tSN̞=+R}]Zn],**b'}Ӈŋӷo_&L/K{1.r^ vlfꫯH˖-:ujJYG[oeĉ4oޜnݺQZZ… ի@6m0blB˖-3fgg[n{G??O_rȅ3F @II ͛7O%h_=vZPDDDDDDDDDpw""W/dlua8H}*`ZjRX0 /SWĆ:\8xѦeͮ2OUEmɺMדllWyYεͮ!&߷q;ml8ej#%?'qbsm0 Ƕmc8N/MDx9`Fx<a`Z9:oSǓi΋D9`۶oaX+ܸinѕ4z)&(b:V]0"''pԤ(?794:۸8@(bb8G4qJ]8#q"Nu2Z_'#bsJt}ORw ;f+06XŲ"6s, F!9"qbY6zZ׻o˵l6xX[ jH׋஡var8X06۶!~̴m<++ޝ?_ \1aahW>_|l1l!j`;Q"獭cJZxy99DL3%7ޖD[9^/x\9kmX;|X@l O-vȩk68zh33^c:‘(7]/e>elr},pMOLBi;,Z9P>pl</axy=DMQ}vL&7 l:8 GMrbs1DM |>ljtp-_Q\Obmۊ5;L`[ァ5qyhoN|O%Σ];`^|^ol ʶ0Jl3W"""""""""" ޻3j(ݻSDDDDDDDDDA%%%l߾4EEEty?E$"""""""""?ڸi`tF"""""""""" 6nEDDDDDDDDDm܊40ڸi`q+"""""""""hVDDDDDDDDDƭH[q6cﶜL&{XFC8efFkvL꓇z[K;2akS)eA p[b.fZ^]]i?.t[OrŰ/Z=.#qqm־2:^yG19gck R:о:EleL}oo}\IZkuG6<ޫdxTC_""?0/FF-izdT]M{Z84ɱ,ȲNugNlgި#VV2|~MLN&w}`oOc;Wd6^^5P}MJ琈NZ# ֌qH[F"""""""""" 6nEDDDDDDDDDm܊40ڸi`q+"""""""""hVDDDDDDDDDƭHsHl}|> 0 ֭[wCg~<#t8i}s5PDDDDDDDDDJdvѢEk;m۶ѦMVfC5|>FVXAyy9~EDDDDDDDD;y睜|g?kesGe˖VfCճgBʎ;83t("""""""""qsN~as' e6D p : ŕU:ʲ~s\FF?Vѽ4ú6j0%]}'sIFL_mvE>z͵X^q:gM[ޟe6mMҮKĚ.Aumx<4߇lQ4|cd_ҥ>ʴ=5k#p}Vp02>'e*VW5˶E85f'syxIK^@lI;k:ϕ=9%Lׇ.VV{ǒmbhU[_jwS[}ݩzqedtwugmkdP<^vCyO.E,ƃaeaxkȦo$o>$km&֕$o6dwo{aoVYYFcǎЧO͛%\Baa!v?uQҮ];Jnѣwq{֕+WfK8(**⬳bҥ֚'GP\\0 N:$0iٲ%EEE~;*"//0((( rӾ}{ٳ'ovڲ.\H߾})**ÇS^^ .q[ܶߟ۷_͛S\\yǪUҖvZ F֭),,ce̘1TTTpꩧLO/Nƍ֭/BژDDDDDDDDD5n:yꩧ8쳹뮻q8رceqyOpw_2aM駟NUUl{<\uU^իWsGgΝ;931c_~9SN 2ʴynVj?|.]̙3Yb~:˖-`ڴilذl¨Q0 '|s=ŋg]/~ ޽;PQF~z&Nmʕ+8p }QJK,s/`<33h 5La„ ]˪U1b;w;^z)sΥo߾ׯ_OϞ=/?>%%%s9ɓ'Lo83h۶-k׮;2b֯__9zO>E1p@}Y v#

m4-Z}e]@ΝyǙ2e GCnf͚ѩS4i7n+tuQ\r%5ߟХKp3~xf̘NiҤ <[NN0`~jٔ]PP '?oo]TT_?)뮻D"L'""""""""r(6m?uy)i=SS[@yy^ bFԸZ7???:ml7trKjz:e˖QRRBǎ]vǎS'6#C{lA:t@QQQJΝ;->xjgx(//K.56f6wЭ[7~iϟϵ^˕W^I&M2GDDDDDDDD`Wm߾}޽;3fફSNo#pWM[vi'7WM@mM85Kk=e=,v&Ǔ8Q'۹s'@ګ3i|EEEyOs'N/w"""""""""Z7n^/ͣwޜzGc=5\6EYM4}kQPPĮ|hrrrmǎ5O|[beg"ե;ϾծW`g+4iĉĉ9r$m۶ADDDDDDDD`P=nz)zώ;_Jnݺ駟m֮]rܶ{'d{&B!JJJɡ}{Tv&ڶmK~~>6lH/5k֤ͳ/9(..fݺuzi|`вeKN̙믿^EDDDDDDDD&unN>޽{O_cvsSLaݺuժWկ_?fΜr|ܹTTTH/Liiis˖-7H[S ۲Mu?{^:,i+͟8da 2h4O?]#ݬYjKΟM|s7xc't""""""""":7nwθq8ٳ'zfر)W]^tE ͛7pUW]Σ>7k<رc2d}]vw}ӷo_{9fϞ͸q83o6''oYf裏r5אÝwYK.O>I۶իWq+GW_e̘1lذ۳yfJJJ5k֬˲ܸ[lYiӦ 7x#^{-/2&Mo߾)~!+VH٠4VZ|rx eee1|𴱊Jjݸu /qXl~)| .Ǐgȑ)g̘˺u>|8W\qnMw;>^z|n֬C ᩧ׿5cwqWr3(..keС̝;g}Ԩ#2sL~?W^y%7|3]ta޼y'%m&eYf|Wn>F7ny[z̟?]r-pWR^^γ>KD"q\}Yo[9wSNsf͢۷ЦM>Fo~vO?M M61-[вeKƌU|G[oeĉ4oޜnݺQZZ… իWCDDDDDDDDa8ئ/K[NqGNNe7pl2v~iաk׮Z ADD-=E 3iI[m|z qy^ǩ,ro!|uI8!9OI:۶x;b[?pSZ6U_ӥK~ͺڎԓ8^ O޽yy7xgѣ{ҹsg IJeضm!qK"{G}Τ'm9AǦ9)?\Fh&JDzćl$m&ۛnf^.L"Yšs4Ħm]53/ ˯-MݝَQmyY2){oi[=ϞĽ'F}5^ٮٔL_CܴH~i0`/j7ټTu6Uz2+8$6m V\I˖-С^ ⥗^r/ ذM`!{s<Ӈ={8֭cɼL6 /mߟ?|J}ٸq#7n`ɒ%g?ۧ7oog޼yl޼C۶m9SO~cݧn}є+m/b^yպq+""""""""""Fü+6nEDDDDDDDDDm܊40ڸi`q+"""""""""hVDDDDDDDDDƭH[8I?0_r_f6)l3XՕM"?x|ic[8uJC˄_Ci4ODPᠧ[F"""""""""" 6nEDDDDDDDDDm܊40ڸi`q+"""""""""hVDDDDDDDDDƭH`7n7l@qq1a`ӟtH5OΝ91M@#"""""""""uhGqeee3@rHؾ};k֬aJJJxgPd"""""""""R]ݸ0 ^z0 :t^`֬Y4m4)ShVDDDDDDDDd>|xp8̜9s0aݻw?@Q4,/:!sú6:!4Ww(c`vx9axd}]m _V784quhHΕiO@}xh8V<{)Ͻi?ݹs'O<Guwq<5򕖖rWЩS'h޼9gqK[ڵk6l[c=1cPQQQg'p'|/~ ڷoO~~>ڵ㢋.bժU51M|nݺQXX1ɓ뮻h֬}MIOңG (..f|guZ à]v|^z)Z²,؀􅅅L4 ȼof7ϷUW]Ő!C,YMOf&a9T5ONe/]=aJZ~""s{,5\Ν;ٳ'{/gfL4F<;.c˖-矧]~=={/dp9C1z:^`nݺ_wĈ̟?gy2ϟϒ%KOJ(J)ooM6}{챌97|wl߾M{뭷rWstRfΜɊ+8YlY]3m4?x6mDn̞=m۶`7O\ ~ewW>O<իWzj q{DDDDDDDDDd ={6ÇgĈ̛7Bn碋.?)W_}57]v\veض͛oRmFyy{k͹ۀؕuLNxwڵksF/sϥݻ3`6mĒ%Kt#мys̙G^u:vw}]wҥKo߿?ǏK.׏/AƏI_ `ǎ)~m9sfs>sGDzN:ѺucZ# //N:ѩS'4iU{DDDDDDDDDdva?ݸ袋xv%mѢg}6)?OC-cȭ[a4jԈ޽{[jŋ:uj@?)$\wu)Űyf~?{,yyyn{?xlªU7Ķm Rn>qԥu?$ ӟԩm_b)d SCFwԩSǾ:,xGϧ*v{Q^^N.]jl&'m L2?L0;MW_#d l޼F %$j=rJn&nu. O?_]t! 2g&NHII <-3Ϥ δEDDDDDDDDz,ݻ7{/7p_~%˖-wΝfE=z4^|m۶HtRzܹsygXr%K./#G|r|MLdż{wymM[YY ĉYtiʿիWguۄջb=я~Đ!C]K7o=zHA6-"""""""""~/cW_}իW_/M85 7qWe͚5;̘1x뮻ӧy;O>~a֯_1 mrWswkԨ999i>SN9-Z`lf֬Yn'?]vk<̞=??%6-"""""""""]zu,())[n)Ǔvan:~F:^zqwСC^z%+{=V^ͺu먨`ժU<4m4%]O?4X3x8s> /b ())>c޼y 4(m2mkmt['3f`xӦM{4nܘ'o-[wN0 !CFySrYf۷/=gfܸqydq2}uY4oޜiӦոMB}o۶7c믿[n9-"""""""""qSQFSO1|>~inJǎkkRᇌ3KҲeKxJذa<˗/ww7sm6ZhG x8 p a.\H=? Xn<=7cǎѣy㏹袋ذaCwЁݘؗM8˗8ݻw禛n⢋.vi_#8".S2o<~ți[)Wٓ?뮻˗Ǹq&ً+3jXF:`8V8ݮCڇهq7~moq&k1&:ն(=[ uO{C1?ZwZmތ6nz-'NfyPZZM7ԩSy7^*"e3oO*/`{`kmzekjV{J OC+6n4 ?~!U{ΗI"۶u8u] <;/֭[WnݺE("ʡc~(op-4~gc"^,в@ uO{C1?-ڟ2Q=nu\{Pm`0rd+WW^e s6ȡ+nC~)K,9nz6l//2>lݺz[oѣGs 'd\f-w裏ޓEDDDDDDDD =ng̘k"{e˖ԭ"?/k׮Ŷm7oΉ'q%EDDDDDDDD/'{܊[F"""""""""" 6nEDDDDDDDDDm܊40ڸi`q+"""""""""t"" ex}-yh%mKk{:adQW;oQ{<m2[0|tӬSI _Q7 ht퀁ΪXVD+x8@mA[*E-3A;6k_^´M ۿj56mN ێktv/)>; ; {fxSͻmkag7G(VO^!eK&1<8(ޢfgMP9YıXJ*\F9'=4ǪDUKVءƍǶm m y50,O<}"iؖűm<^/^i,l!PQm@ƍ`4)/ `mzmq0- χ8x9! )505MIݶm0 ,"''۶,kW쎃87mw/,r'5 í׶mlM0pD"xb}8H1pİ-r54MmC|s HeANiyXm Njx|۲p 6.>H?Yr eaY&;17j}cqωxxz^ߒ,q o|1JQm<e$4Q0dq4MAv|+> x<7h4{X H(Dn~; #g- c؎; Ϯ O\aB| ζ-]cjogb۟^o||zDQ |plq_'yx;`Ƕka&O/1QeY|{xljur{ FbH3`VZ^cYqurh0^/^][ s6ѨINN ɉ=gYXIŶ,,Ʊmr- 3>gmۘ{lq\ 'җiFq% g9Dz7ohǃݵ6pn|Ķs111ٖ/''|H$⾦϶5"1d"u' sƶ_w C{}5k1&uyOސ]]6nqL*`e>fzlN9mE*0Lvu1+oZ[*ˢ8]o0Zؕ_XU_ʩ oKw; gXEj9E f>g&߭"'eUcڼuk;B/%P; VS6B J,Ue`8̮-2q,'ŷe-Ʀ Jn]DciTVVb Ʋ"Plc f4;v`n!/ bu,l"c%m;qcDD"~hH8Vm80 ݲBeF `0]D6H۶ n:˲$~o Vbs $XQFtkceB4V4C;Ʊ,"8 &v$ vͶ aGD1]1!Ю%o`l$&iX-P(M(oR9V`wu$qvO;& c&~Ŷ~6f8++v7?WRR35-?8u{'7o}{|ߏOq^}wi|\pA߿?w.`%7} ^zl71袋K/k _?m>{󞈈}s/}i("?jꪫ΋ɟUVƻ׽U߉<)|%xc_Wg_8Ķm?A\qqW+^^swN>[9yNDD\veq饗ƛ8SD|򓟌π}#]w]LOOǿ\yk~׎xՋ/8_yC{^\xq5ĕW^y9眸[3Ϝ/Ї>4{E#D5kҗ]w]tܦ}x/zыbڵG?:""/"wƳظqcr->g?zիfv)t~?q7FӉR^lذ!wźu""%/yI|󟟽F-o'=IqEEDD\|g>3.عsgDD|G=QqgDD//ţxC:{ybt:8=yOկ믿>N=Ըӟt{׾6nG>x#""o~s\}ձ}x_~СC׿>:N|c~yZŚ5kfk8쳏;gyf\wuGɾ}eYFD 'v@+Ղ ?xlhqns9'~ǵ^{zشiSD/8EDO<197_җ""+_J|[ߊG=QmDW2{G>(2?yYgED 6g?ٸgO}*^ǩW_}ulذ!"U|{k6=^]w]r-qgφccc9o =k?k/-oxNϾ&''g=֞={""x]l~k_;jsCʈN:i.ψ]vED_v<[nezի[o:+6m4UA=w?ar-Gv.;~7~z#K`p}T\szLw{ڷ7_ۿ?qDDzGx?xx?{sٟx^;w;wy}{SNYku>~E9ξ>餓~VE} nQlPullvq=qԟk{nVZsT=\sMuԧ>,58^k|n.6Nd_Yԣ{ܟȃ<o.dD_pms\|qYgsύk6,5M9SOn)gzfq睴s]~t_.]`~ nwyݙ7tS|ӟO"{9g??/"8("N8Ef͛7GD~R7 3ό 6g>+b}tIaÆ7;n)>Ond_FyG|wm[Tp{??@133ykzVm?۽{wG?w_SSS?>wg=Y(/ 8o|cܹ3ַƙg9i7q-xW_}u>jժ8\{׿>nzO}*ůʯI',VY ??<79^W_s9*>'>1t7^̾/}iܹ3N:x_6m?~q'Yg6l?񒗼$m۶,=y{""⪫ 6ċ^w|s=y8_y²??wqСxs/{я~toqk?ַۿz׻/g?7!s4ޗ}㑏|d/ؾ}V"1G?߮i)ګ5+cX1:χms#bx}3vnX5>>>EYӂRDQİ{,h#2Rtb<]"u֮^YFQCT5EөfYE':5>4Ӌju)"2Ɔ^))"f*vZReDcRM8Kzb)ʔb|k0wZT/9wOdF eжj[lmpi)ګ54ڌBhC"7hUF.7u(rQt:=N͐zxM׻>nc[ ]݅ڬӷǛcc Yy8XcRS m#_BۈUƪXhCۈNڶl|?/TKyA5w~uIOz*:XA3v +dwm6j^2=+mqq:sxǹ:_턷d61uϕR7m\eBǥL)vvGw;?W5+M>#Qa̫O?m}o.3g2эT\}Ckn]ݩn嚛|R6;߆]{.m]w]DD\{|f~퇟rm^W5Van2#Ȍ 3[n2#Ȍ 3EJ)]МEE(,tTeDD},Xeܭ^NVW=֪c]oއ""FIn3v9ٶk_%MMvҎ7+CjX)MSݑZQGxcy;cz[otc2>w_ҰJi e.bJrǎZw["Ȍ 3[n2#Ȍ 3[n2#Ȍ 3[)virm׾K]^ꜪFOMv+cucρZWU66v=e;곅}ŖnLWo޽V~Gs>޴q]w:QTc93̹rć~ڋh~̪x7qMl׾(jZmic [J)#"W޳ҘcaMV/or pQ 3[n2#Ȍ 3[n2#Ȍ 3[nX1^ފ ʹGEJie\Fx@v wkjv dչUVGm0R=Ea|rT=qX'"駭+o8Xi6ݺ; rR(y1h=D>K)36?E*#31{W7Otc٨W] -@fdFp -@fdFp -@f)RJ"`J)EQm1_-Vx("̎bRK+FJ)fʈUc|[iiڶJ&Ri7s!vz^ Q+eU?oWek0) T^ًN'KGt] ϷŶX.ڰm׾KXRSݶK` s2d7zecȭS5oٸ.h,v>R"-+3eHEaݩ:ιŎ!Ή6LMv#E+b("GlO['uvu}Z{eubb6O;uMHqՍ36vcvLqŞa}ٴq}Ѝn[.W#Jp -@fdFp -@fdFp -@fRj&"N;*_׋"b΀R߮,a(Sa e蕽ttAgu|iK?PlLp 07&Ckk!MdwN]F֦&ROMQkUͪ_<>y9FXlitLӻXcǖn#sbl:}]l߽/*|ж,ߏXTƺwռ]HPRgLph5[n2#Ȍ 3[n2#Ȍ 3[n2#LRJm.m۵}MMvRj꘧&;>^Z6o\cNew(~kf:y8j{vSJfzq r=?SD 'yJ`*\Dï+˨Darj-@fdFp -@fdFp -@f0#TݞH)Y22v nۮ}Ckkj;1OMv)(>ֻub}l߽V9F,czZ}ib}\^?͵7ӖֱX_ꞿ['GQqٮ}qn\gx׆*s/nL8#qSs70t|oJȎ 3[n2#Ȍ 3[n2#Ȍ 3[)v(RWeDDt:;JNsmORDTv*S΀}Z8+qGcTljunn,URR&Jc( PÚNp 07&Ckk!MdwW~v sqlXX6~:ٍCjQ:?k㪯Bfzs):ٍ":׋a}}Zry06փ~:؆><3n2#Ȍ 3[n2#Ȍ 3[n2#Ȍ 3EJ)]lff&oJ)RJTjEQ4U@cRDXi۶kښ,1OMvڗM۲q]L9vK?Fa=WRJQE)-U~Ǽ}XsjsyREl9?eY\̡tݞ@Vụ^dFp -@fdFp -@fdFp ".;]۶k_c6TY~EQ4QVL.c^SJ:Ȝ:o>/#EĠ3GN3oMQξl}Tu D8~2эCrn\z=u%ΧxT+/wJe5c1dFp -@fdFp -@fdFp S]"k "|)GhEJ)]wom׾55ml_Kc6ðe㺘sҶm\hW9Ph^-ݘ]Q\k:e)HqCoj{Y[MK}N:+v8ax̔ewCvO)E":5'U33Ea͆&*~Y9NhW!Nz5ړCP7.m۵ohmMMvB: Nl>}m{FQS|uVU%(2.|=^bzpϛg&Vw:Lo&{\ҔRlVJe*cS9Ϸaڊ(×![dFp -@fdFp -@fdFp -@m):Q]HSEJ)]wom׾KPkud7RJ}Fj:vMkSI9Wm,sǷ8mXcGn1RlF(]wyL\Zkh\Եq ݊M]rK)Ɗ}SŪN;`hn_<* 3[n2#Ȍ 3[n2#Ȍ 3[n2SRE鿍.J)%se+SD"ED("E(Θի.cdEӉii[.n"tȲ!Ȑ`);ǯȏJlܶ]ZiwjJson?4=SwNWڶڗM*ulXۗ\:כ:Q&Ȍ 3[n2#Ȍ 3[n2#Ȍ 3EJ)]GK)EQmG㝖)"\kܶ]ZmjJMWg?M։!]{nv5u87RNMy+%|jbM)b|l,NEQMdr8^uDyYؾgu>gz)vWWvWO{w0rc廨Y+)E8R(3]f.`^] n2#Ȍ 3[n2#Ȍ 3[n2#Ȍ 3EJ)]wom׾KlWgLcc?nl`,ú\W1֪XEԃ|}Z7&օ&֦yرgkWi3"L).۵?ƊisFRL6}䮉ڴq}\^q7e)M#'C7Ϻ(Q Zn\Ҫf0}wϗ'Ȍ 3[n2#Ȍ 3[n2#Ȍ 3[`VvVjgO+"x0m]BLMvfݯ?cؾ{e E9Wu>nhz9 smnMMvk38n\/MıqTԵkRz<* 3[n2#Ȍ 3[n2#Ȍ 3[n2SREդ(ˀeWRaB[rv9Ga,.RX^g#D]l۵&Coc_ӻW޾q6lBjb;oudFp -@fdFp -@fdFp ".'EQ]F-*2:x1ZRJ+s6`dm۵&1W{}9N}['φJ:a?~MU>h SlJQϧ&R28reb}t3e+3?χURuu]oZkb>/ƹjuKXZK(l-տKEp -@fdFp -@fdFp -@f)RJ"xPͶ]Zk{jJMcߩn0_i#l q4=!x[en[ܯsξRgnz}Pw &QӻRñxW M] rZ5u\ nS<*,zJˈm0+eT 0|[n2#Ȍ 3[n2#Ȍ 3[n2# KE0dxjYEFWoٶkۜ͹8RJQqyRAXAdwhstGmtڸ|iz-mmI)E":ZO[W0ظedeb}D/ͪ5`1RM?3e9'x^pں^w{d-@fdFp -@fdFp -@f)RJ"RJQŒ?"QcTRD^K<2/ffbxm{ec߫faXzeNsL^/֞}R`v;re<DzL): ҩ۶kۜ͹rdFp -@fdFp -@fdFp ".f(e 3GcYs0xPͶ]Zmj;68N)J Fl\{]@s޿o|jH1=625m}jk:ٍ^;0>MM]:NQDLQ)E3t}2э֒ƮN[&1P}#cRWKGp -@fdFp -@fdFp -@f)RJ"xPͶ]dwm1{,mgdrU:>?r:ܤQAT'\d2v90pǶR|W:fu֦~͜MM&݅aSjnrY;5_6u,M΃˵v9Ȁ0 3[n2#Ȍ 3[n2#Ȍ 3[nswˍ@wR%r׾RGF[@ss:b?ao_ߞgp?N.'1xzo2[|!=jٶ;g-ù|U@1[bn#(Fp P-@1[bŴE𴌈6fgmDD&3a_7!3nx23`<9]Ӝ_^jy>wxFC9UcOݓS?ZW˃ճmSㇸVKr_bTr<)ks^GZ०sk^>M<=u(> P-@1[bn#(Fp P-@1-3ste gCɈpq:9zx}=c^-zk=u !rا٣mZfFĔV5{gc㙝[ܽ^-vjzZj_78=9nynhzދ/w {m|UG`~h /."1'N<@1[bn#(Fp P-@1[b]޵F@Dzk"#b3 Zf9]Ӝ_^ZϞc_Զ?:%=:sƽ=shgk{Wn}tݧ!aOO7MƗ-=}cqz;o2bzG-{~gXߧjn~mԳW%#(Fp P-@1[bn#(Fp PL]/[k{g}18ԙ;n6q63 s U{sLȈp:]Ӝ_^{Zǘ}jrZEBz:L3]{ EdFFk~u闛`~x':eoޚS{o<{^QShvo7fr~{m_|-@1[bn#(Fp P-@1[bŴEeff??ף8HsjneD8c n\s׈1Ogqqu3xθe欺_kZN^g`V? jܝ`J;Mb1=gFu`_cz)}K͝;Wd{弎J|.ޖl4cn#(Fp P-@1[bn#(ef.oNG4sZ]=|,S^ZF{sv>:Ϗkko9uZƗǏ=x[Ogڷ2s9wSvjsN۶6hErhc{퇏d켏=ܽ5gۣ/{fX{9mi*2#Sq{^gߊN6-@1[bn#(Fp P-@1[bZD*g=V8Otjk|.*8zn̸ǔ:33֫ef<ͱ;LJXL?k5~!Zkzsn{oh}23߾_'=?us[b_gg=Cdė}U|U@1[bn#(Fp P-@1[bŴEO.ݧ>5Ʒ8̌kwߍ.wj7}TC 2 W 23.n&=^-zVE9̩s[qc:w{8z)L?#k㧳1=zumK=uU>!OzAw{;n7IDAT#(Fp P-@1[bn#(ef.`WfFkmteDxNG_^.!""֫?R㱆)۹3OkLa6M,z{vZ*glh&3\ݼ1=sM{ן&{=zuv޺\u_bbhۿ:[-@1[bn#(Fp P-@1[bZf"ttP""bZ>9u~tW7]hMwsRs_KCp=wLk1{{հkn=cs6{XωwX*-@1[bn#(Fp P-@1[bŴE%z|gs\٣5ѫ95z{ȳik܅}>s?z[[bn#(Fp P-@1[b;٥֥z6Z\Zfe 9]Tq~y=X̌YOgkl2ԹYǔzZ>c|9czxz5c!{v%>|U@1[bn#(Fp P-@1[bŴEh mc=x/Ŝ.8]BW'>mS,Zk_]e8F0Տe|0u^ˈ*]vٳ}~{_{9xNU}|-@1[bn#(Fp P-@1[bŴEO#(Fp P-@1'In{IENDB`tracing-durations-export-0.3.0/src/bin/plot.py000075500000000000000000000233231046102023000174750ustar 00000000000000#!/usr/bin/env python3 """ Installation: ```shell pip install "drawsvg>=2,<3" "pydantic>=2,<3" ``` Usage: ```shell python plot.py traces.ndjson ``` """ import os from argparse import ArgumentParser from collections import defaultdict from pathlib import Path from drawsvg import Drawing, Text, Rectangle from pydantic import BaseModel padding_top = 5 padding_bottom = 5 padding_left = 5 padding_right = 5 text_col_width = 250 content_col_width = 850 bar_height = 20 multi_lane_padding = 1 section_padding_height = 10 class Instant(BaseModel): secs: int nanos: int class Span(BaseModel): id: int name: str start: Instant end: Instant parents: list[int] is_main_thread: bool fields: dict[str, str] def start_secs(self) -> float: return self.start.secs + self.start.nanos / 10**9 def end_secs(self) -> float: return self.end.secs + self.end.nanos / 10**9 def duration(self) -> float: return self.end_secs() - self.start_secs() class FullSpan(Span): """The full span from start to end, including the holes where the span wasn't active.""" pass def main(): parser = ArgumentParser() parser.add_argument( "input", default=os.environ.get("TRACING_DURATION_FILE"), help="The ndjson file generated by the rust process", ) parser.add_argument( "--output", help="The name of the svg file to be written." "Defaults to the input filename with `.svg` as extension", ) parser.add_argument( "--multi-lane", action="store_true", help="Don't overlay spans", ) parser.add_argument( "--min-length", type=float, default=None, help="Filter out spans shorter spans (unit: seconds)", ) parser.add_argument( "--remove", nargs="*", help="Remove this span name (multiple use)", ) parser.add_argument( "--inline-field", action="store_true", help="If the is only one field, display its value inline. " "Since the text is not limited to its box, text can overlap and " "become unreadable.", ) # See http://www.cookbook-r.com/Graphs/Colors_(ggplot2)/#a-colorblind-friendly-palette parser.add_argument( "--color-top-blocking", default="#E69F0088", help="The color for the upper section of span active time when running " "on the main thread", ) parser.add_argument( "--color-top-threadpool", default="#009E7388", help="The color for the upper section of span active time when running " "off the main thread (with `tokio::task::spawn_blocking`)", ) parser.add_argument( "--color-bottom", default="#56B4E988", help="The color for the lower section of span total time", ) args = parser.parse_args() spans = [] for line in Path(args.input).read_text().splitlines(): if not line: continue if Span.model_validate_json(line).name in (args.remove or []): continue spans.append(Span.model_validate_json(line)) # noinspection PyTypeChecker last_end = max(spans, key=Span.end_secs).end_secs() full_spans: dict[int, FullSpan] = {} for span in spans: if full_span := full_spans.get(span.id): assert span.end_secs() > full_span.end_secs() full_spans[span.id].end = span.end else: full_spans[span.id] = FullSpan(**span.__dict__) # Remove to short spans removed_span_ids = set() if args.min_length: for span_id, full_span in full_spans.items(): if full_span.duration() < args.min_length: removed_span_ids.add(span_id) for removed_span_id in removed_span_ids: del full_spans[removed_span_id] spans = [span for span in spans if span.id not in removed_span_ids] # In expanded mode, we avoid overlaps in different lanes, so we track # until which timestamp each lane is blocked and how many lanes we need. lanes_end: dict[str, list[float]] = defaultdict(list) span_lanes: dict[int, int] = {} for full_span in full_spans.values(): if args.multi_lane: for idx in range(len(lanes_end[full_span.name])): if lanes_end[full_span.name][idx] < full_span.start_secs(): lanes_end[full_span.name][idx] = full_span.end_secs() span_lanes[full_span.id] = idx break else: span_lanes[full_span.id] = len(lanes_end[full_span.name]) lanes_end[full_span.name].append(full_span.end_secs()) else: span_lanes[full_span.id] = 0 lanes_end[full_span.name] = [full_span.end_secs()] lanes_end = dict(lanes_end) extra_lane_height = bar_height // 2 + multi_lane_padding # For the left sidebar, sort spans by the first time a span name occurred earliest_starts = dict() for span in spans: if current_earliest := earliest_starts.get(span.name): if span.start_secs() < current_earliest: earliest_starts[span.name] = span.start_secs() else: earliest_starts[span.name] = span.start_secs() earliest_starts = sorted(earliest_starts.items(), key=lambda x: x[1]) # Top row is for the timeline name_offsets = { name: index + 1 for index, (name, _start) in enumerate(earliest_starts) } extra_lanes_cur = 0 extra_lanes_cumulative = {} for name, _start in earliest_starts: extra_lanes_cumulative[name] = extra_lanes_cur extra_lanes_cur += len(lanes_end[name]) - 1 # Don't forget the timeline row total_height = ( padding_top + (bar_height + section_padding_height) * (len(name_offsets) + 1) + extra_lane_height * extra_lanes_cur + padding_bottom ) d = Drawing( padding_left + text_col_width + content_col_width + padding_right, total_height, origin="top-left", ) if args.min_length: # Add a note about filtered out spans d.append( Text( f"only spans >{args.min_length}s", "1em", x=padding_left, y=padding_top + bar_height // 2, dominant_baseline="middle", text_anchor="start", ) ) # Draw the "timeline" d.append( Text( f"{0:.3f}s", "1em", x=text_col_width, y=padding_top + bar_height // 2, dominant_baseline="middle", text_anchor="start", ) ) d.append( Text( f"{last_end:.3f}s", "1em", x=text_col_width + content_col_width, y=padding_top + bar_height // 2, dominant_baseline="middle", text_anchor="end", ) ) # Draw the legend on the left for name, offset in name_offsets.items(): y = ( padding_top + bar_height // 2 + offset * (bar_height + section_padding_height) + extra_lane_height * extra_lanes_cumulative[name] ) d.append( Text( name, "1em", x=padding_left, y=y, dominant_baseline="middle", ) ) # Draw the active top half of each span for span in spans: offset = name_offsets[span.name] # Show name, duration and fields tooltip = ( span.name + f" {span.duration():.3f}s\n" + "\n".join(f"{key}: {value}" for key, value in span.fields.items()) ) x = text_col_width + content_col_width * span.start_secs() / last_end y = ( offset * (bar_height + section_padding_height) + extra_lane_height * extra_lanes_cumulative[span.name] ) width = content_col_width * span.duration() / last_end height = bar_height // 2 color = ( args.color_top_blocking if span.is_main_thread else args.color_top_threadpool ) r = Rectangle(x, y, width, height, fill=color) r.append_title(tooltip) d.append(r) # Draw the total bottom half of each span for full_span in full_spans.values(): offset = name_offsets[full_span.name] # Show name, duration and fields tooltip = ( full_span.name + f" {full_span.end_secs() - full_span.start_secs():.3f}s\n" + "\n".join(f"{key}: {value}" for key, value in full_span.fields.items()) ) x = text_col_width + content_col_width * full_span.start_secs() / last_end # lower half y = ( offset * (bar_height + section_padding_height) + extra_lane_height * extra_lanes_cumulative[full_span.name] + extra_lane_height * span_lanes[full_span.id] + bar_height // 2 ) width = ( content_col_width * (full_span.end_secs() - full_span.start_secs()) / last_end ) height = bar_height // 2 r = Rectangle(x, y, width, height, fill=args.color_bottom) r.append_title(tooltip) d.append(r) if args.inline_field and len(full_span.fields) == 1: text = next(iter(full_span.fields.values())) d.append( Text( text, "0.7em", x=x, y=y + height // 2, dominant_baseline="middle", text_anchor="start", ) ) d.save_svg(args.output or Path(args.input).with_suffix(".svg")) if __name__ == "__main__": main() tracing-durations-export-0.3.0/src/bin/plot.rs000064400000000000000000000052531046102023000174700ustar 00000000000000use anyhow::{Context, Result}; use clap::Parser; use std::io::{BufRead, BufReader}; use std::path::PathBuf; use std::time::Duration; use tracing_durations_export::plot::{plot, OwnedSpanInfo, PlotConfig, PlotLayout}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { input: PathBuf, #[clap(long)] output: Option, /// Don't overlay bottom spans #[clap(long)] multi_lane: bool, /// Remove spans shorter than this, in seconds #[clap(long)] min_length: Option, /// If the is only one field, display its value inline. /// /// Since the text is not limited to its box, text can overlap and become unreadable. #[clap(long)] inline_field: bool, /// Remove spans with this name #[clap(long)] remove: Option>, /// The color for the plots in the active region, when running on the main thread. Default: semi-transparent orange #[clap(long, default_value_t = PlotConfig::default().color_top_blocking)] color_top_blocking: String, /// The color for the plots in the active region, when the work offloaded from the main thread (with /// `tokio::task::spawn_blocking`. Default: semi-transparent green #[clap(long, default_value_t = PlotConfig::default().color_top_threadpool)] color_top_threadpool: String, /// The color for the plots in the total region. Default: semi-transparent blue #[clap(long, default_value_t = PlotConfig::default().color_bottom)] color_bottom: String, } fn main() -> Result<()> { let args: Args = Args::parse(); // Read input let reader = BufReader::new(fs::File::open(&args.input)?); let spans: Vec = reader .lines() .map(|line| { let string = line.context("Failed to read line from input file")?; serde_json::from_str(&string).context("Invalid line in input file") }) .collect::>()?; let end = spans .iter() .map(|span| span.end) .max() .context("Input file is empty")?; let plot_config = PlotConfig { multi_lane: args.multi_lane, min_length: args.min_length.map(Duration::from_secs_f32), remove: args.remove.map(|remove| remove.into_iter().collect()), inline_field: args.inline_field, color_top_blocking: args.color_top_blocking, color_top_threadpool: args.color_top_threadpool, color_bottom: args.color_bottom, }; let document = plot(&spans, end, &plot_config, &PlotLayout::default()); let svg = args.output.unwrap_or(args.input.with_extension("svg")); svg::save(svg, &document).context("Failed to write svg")?; Ok(()) } tracing-durations-export-0.3.0/src/lib.rs000064400000000000000000000364711046102023000165160ustar 00000000000000//! Record and visualize which spans are active in parallel. //! //! ## Usage //! //! ```rust //! use std::fs::File; //! use std::io::BufWriter; //! use tracing_durations_export::{DurationsLayer, DurationsLayerBuilder, DurationsLayerDropGuard}; //! use tracing_subscriber::layer::SubscriberExt; //! use tracing_subscriber::{registry::Registry, fmt}; //! //! fn setup_global_subscriber() -> DurationsLayerDropGuard { //! let fmt_layer = fmt::Layer::default(); //! let (duration_layer, guard) = DurationsLayerBuilder::default() //! .durations_file("traces.ndjson") //! // Available with the `plot` feature //! // .plot_file("traces.svg") //! .build() //! .unwrap(); //! let subscriber = Registry::default() //! .with(fmt_layer) //! .with(duration_layer); //! //! tracing::subscriber::set_global_default(subscriber).unwrap(); //! //! guard //! } //! //! // your code here ... //! ``` //! //! The output file will look something like below, where each section where a span is active is one line. //! //! ```ndjson //! [...] //! {"id":6,"name":"read_cache","start":{"secs":0,"nanos":122457871},"end":{"secs":0,"nanos":122463135},"parents":[5],"fields":{"id":"2"}} //! {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":122433854},"end":{"secs":0,"nanos":122499689},"parents":[],"fields":{"id":"2","api":"https://example.net/cached"}} //! {"id":9007474132647937,"name":"parse_cache","start":{"secs":0,"nanos":122625724},"end":{"secs":0,"nanos":125791908},"parents":[],"fields":{}} //! {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":125973025},"end":{"secs":0,"nanos":126007737},"parents":[],"fields":{"id":"2","api":"https://example.net/cached"}} //! {"id":5,"name":"cached_network_request","start":{"secs":0,"nanos":126061739},"end":{"secs":0,"nanos":126066912},"parents":[],"fields":{"id":"2","api":"https://example.net/cached"}} //! {"id":2251799813685254,"name":"read_cache","start":{"secs":0,"nanos":126157156},"end":{"secs":0,"nanos":126193547},"parents":[2251799813685253],"fields":{"id":"3"}} //! {"id":2251799813685253,"name":"cached_network_request","start":{"secs":0,"nanos":126144140},"end":{"secs":0,"nanos":126213181},"parents":[],"fields":{"api":"https://example.net/cached","id":"3"}} //! {"id":27021597764222977,"name":"make_network_request","start":{"secs":0,"nanos":128343009},"end":{"secs":0,"nanos":128383121},"parents":[13510798882111491],"fields":{"api":"https://example.net/cached","id":"0"}}``` //! [...] //! ``` //! //! Note that 0 is the time of the first span, not the start of the process. use fs::File; use once_cell::sync::Lazy; use serde::Serialize; use std::collections::hash_map::RandomState; use std::collections::HashMap; use std::fmt::Debug; use std::io::{BufWriter, Write}; use std::marker::PhantomData; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use std::{io, iter}; use tracing::field::Field; use tracing::{span, Subscriber}; use tracing_subscriber::layer::Context; use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::Layer; #[cfg(feature = "plot")] pub mod plot; /// A zero timestamp initialized by the first span static START: Lazy = Lazy::new(Instant::now); /// A recorded active section of a span. #[derive(Serialize)] // Remove bound on `RandomState` #[serde(bound(serialize = ""))] pub struct SpanInfo<'a, RS = RandomState> { pub id: u64, pub name: &'static str, pub start: Duration, pub end: Duration, pub parents: Option<&'a [u64]>, pub is_main_thread: bool, pub fields: Option<&'a HashMap<&'static str, String, RS>>, } pub struct DurationsLayerBuilder { /// See [`DurationsLayerBuilder::with_fields`]. with_fields: bool, /// See [`DurationsLayerBuilder::with_parents`]. with_parents: bool, /// See [`DurationsLayerBuilder::durations_file`]. durations_file: Option, /// See [`DurationsLayerBuilder::plot_file`]. #[cfg(feature = "plot")] plot_file: Option, #[cfg(feature = "plot")] plot_config: plot::PlotConfig, #[cfg(feature = "plot")] plot_layout: plot::PlotLayout, } impl Default for DurationsLayerBuilder { fn default() -> Self { Self { with_fields: true, with_parents: true, durations_file: None, #[cfg(feature = "plot")] plot_file: None, #[cfg(feature = "plot")] plot_config: plot::PlotConfig::default(), #[cfg(feature = "plot")] plot_layout: plot::PlotLayout::default(), } } } impl DurationsLayerBuilder { /// This function needs to be called on the (tokio) main thread for accurate reporting. pub fn build(self) -> io::Result<(DurationsLayer, DurationsLayerDropGuard)> { let out = self .durations_file .map(|file| File::create(file).map(BufWriter::new)) .transpose()?; let layer = DurationsLayer { main_thead_id: std::thread::current().id(), start_index: Mutex::default(), fields: Mutex::default(), is_main_thread: Mutex::new(Default::default()), out: Arc::new(Mutex::new(out)), #[cfg(feature = "plot")] plot_data: Arc::new(Mutex::default()), #[cfg(feature = "plot")] plot_file: self.plot_file, with_fields: self.with_fields, with_parents: self.with_parents, #[cfg(feature = "plot")] plot_config: self.plot_config, #[cfg(feature = "plot")] plot_layout: self.plot_layout, _inner: PhantomData, }; let guard = layer.drop_guard(); Ok((layer, guard)) } /// Whether to record the fields passed to the span (default: `true`). /// /// # Example /// /// Span: /// ```rust /// # use tracing::info_span; /// info_span!("make_request", host = "example.org", object = 10); /// ``` /// /// With `true`: /// ```json /// {"id":4,"start":{"secs":0,"nanos":446},"end":{"secs":0,"nanos":448},"name":"make_request","parents":[1,3],"fields":{"host":"example.org","object":"10"}} /// ``` /// /// With `false`: /// ```json /// {"id":4,"start":{"secs":0,"nanos":446},"end":{"secs":0,"nanos":448},"name":"make_request","parents":[1,3]} /// ``` pub fn with_fields(self, enabled: bool) -> Self { Self { with_fields: enabled, ..self } } /// Whether to record the ids of the parent spans (default: `true`). /// /// # Example /// /// Span: /// ```rust /// # use tracing::info_span; /// info_span!("make_request", host = "example.org", object = 10); /// ``` /// /// With `true`: /// ```json /// {"id":4,"start":{"secs":0,"nanos":446},"end":{"secs":0,"nanos":448},"name":"make_request","parents":[1,3],"fields":{"host":"example.org","object":"10"}} /// ``` /// /// With `false`: /// ```json /// {"id":4,"start":{"secs":0,"nanos":446},"end":{"secs":0,"nanos":448},"name":"make_request","fields":{"host":"example.org","object":"10"}} /// ``` pub fn with_parents(self, enabled: bool) -> Self { Self { with_parents: enabled, ..self } } /// Record all span active durations as ndjson. /// /// Example output line, see [module level documentation](`crate`) for more details. /// /// ```ndjson /// {"id":6,"name":"read_cache","start":{"secs":0,"nanos":122457871},"end":{"secs":0,"nanos":122463135},"parents":[3,4],"fields":{"id":"2"}} /// ``` /// /// The file is flushed when [`DurationsLayerDropGuard`] is dropped. pub fn durations_file(self, file: impl Into) -> Self { Self { durations_file: Some(file.into()), ..self } } /// Plot the result and save them as svg. /// /// TODO(konstin): Figure out how to embed an svg in rustdoc. /// /// The file is written when [`DurationsLayerDropGuard`] is dropped. #[cfg(feature = "plot")] pub fn plot_file(self, file: impl Into) -> Self { Self { plot_file: Some(file.into()), ..self } } #[cfg(feature = "plot")] pub fn plot_config(self, plot_config: plot::PlotConfig) -> Self { Self { plot_config, ..self } } } type CollectedFields = HashMap<&'static str, String, RS>; #[derive(Default)] struct FieldsCollector(CollectedFields); impl tracing::field::Visit for FieldsCollector { fn record_str(&mut self, field: &Field, value: &str) { self.0.insert(field.name(), value.to_string()); } fn record_debug(&mut self, field: &Field, value: &dyn Debug) { self.0.insert(field.name(), format!("{:?}", value)); } } /// On drop, flush the output writer and, if applicable, write the plot. pub struct DurationsLayerDropGuard { out: Arc>>>, #[cfg(feature = "plot")] plot_file: Option, #[cfg(feature = "plot")] plot_data: Arc>>, #[cfg(feature = "plot")] plot_config: plot::PlotConfig, #[cfg(feature = "plot")] plot_layout: plot::PlotLayout, } impl Drop for DurationsLayerDropGuard { fn drop(&mut self) { if let Some(out) = self.out.lock().expect("There was a prior panic").as_mut() { if let Err(err) = out.flush() { eprintln!("`DurationLayer` failed to flush out file: {err}"); } } #[cfg(feature = "plot")] { if let Some(plot_file) = &self.plot_file { let end = self .plot_data .lock() .unwrap() .iter() .map(|span| span.end) .max(); // This is some only if the plot option was and any spans were recorded if let Some(end) = end { let svg = plot::plot( &self.plot_data.lock().expect("There was a prior panic"), end, &self.plot_config, &self.plot_layout, ); if let Err(err) = svg::save(plot_file, &svg) { eprintln!("`DurationLayer` failed to write plot: {err}"); } } } } } } /// `tracing` layer to record which spans are active in parallel as ndjson. pub struct DurationsLayer { main_thead_id: std::thread::ThreadId, // Each of the 3 fields below has different initialization: // // TODO(konstin): Attach this as span extension instead? start_index: Mutex>, // TODO(konstin): Attach this as span extension instead? fields: Mutex>>, // TODO(konstin): Attach this as span extension instead? is_main_thread: Mutex>, out: Arc>>>, #[cfg(feature = "plot")] plot_data: Arc>>, #[cfg(feature = "plot")] plot_file: Option, with_fields: bool, with_parents: bool, #[cfg(feature = "plot")] plot_config: plot::PlotConfig, #[cfg(feature = "plot")] plot_layout: plot::PlotLayout, _inner: PhantomData, } impl DurationsLayer { fn drop_guard(&self) -> DurationsLayerDropGuard { DurationsLayerDropGuard { out: self.out.clone(), #[cfg(feature = "plot")] plot_file: self.plot_file.clone(), #[cfg(feature = "plot")] plot_data: self.plot_data.clone(), #[cfg(feature = "plot")] plot_config: self.plot_config.clone(), #[cfg(feature = "plot")] plot_layout: self.plot_layout.clone(), } } } impl Layer for DurationsLayer where S: Subscriber + for<'span> LookupSpan<'span>, { /// Record the fields fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, _ctx: Context<'_, S>) { // We only get the fields here (i think they aren't stored with the span?), so we have to record them here if self.with_fields { let mut visitor = FieldsCollector::default(); attrs.record(&mut visitor); self.fields .lock() .expect("There was a prior panic") .insert(id.clone(), visitor.0); } self.is_main_thread .lock() .expect("There was a prior panic") .insert( id.clone(), self.main_thead_id == std::thread::current().id(), ); } /// Record the start timestamp fn on_enter(&self, id: &span::Id, _ctx: Context<'_, S>) { self.start_index .lock() .unwrap() .insert(id.clone(), START.elapsed()); } /// Write a record to the ndjson writer fn on_exit(&self, id: &span::Id, ctx: Context<'_, S>) { let span = ctx.span(id).unwrap(); let parents = if self.with_parents { let parents = iter::successors(span.parent(), |span| span.parent()) .map(|span| span.id().into_u64()) .collect::>(); Some(parents) } else { None }; let attributes = self.fields.lock().expect("There was a prior panic"); let fields = attributes.get(id); debug_assert!( !self.with_fields || fields.is_some(), "Expected fields to be record for span {} {}", span.name(), id.into_u64() ); let is_main_thread = self.main_thead_id == std::thread::current().id(); let span_info = SpanInfo { id: id.into_u64(), name: span.name(), start: self.start_index.lock().expect("There was a prior panic")[id], end: START.elapsed(), parents: parents.as_deref(), is_main_thread, fields, }; // https://github.com/rust-lang/rust-clippy/pull/12892 #[allow(clippy::needless_borrows_for_generic_args)] if let Some(mut writer) = self.out.lock().expect("There was a prior panic").as_mut() { // ndjson, write the json and then a newline serde_json::to_writer(&mut writer, &span_info).unwrap(); writeln!(&mut writer).unwrap(); } #[cfg(feature = "plot")] { if self.plot_file.is_some() { self.plot_data .lock() .expect("There was a prior panic") .push(plot::OwnedSpanInfo { id: id.into_u64(), name: span.name().to_string(), start: self.start_index.lock().expect("There was a prior panic")[id], end: START.elapsed(), parents, is_main_thread, fields: fields.map(|fields| { fields .iter() .map(|(key, value)| (key.to_string(), value.to_string())) .collect() }), }) } } } } tracing-durations-export-0.3.0/src/plot.rs000064400000000000000000000310221046102023000167110ustar 00000000000000//! Visualize the spans and save the plot as svg. use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::time::Duration; use itertools::Itertools; use rustc_hash::FxHashMap; use serde::Deserialize; use svg::node::element::{Rectangle, Text, Title, SVG}; use svg::Document; /// Owned type for deserialization. #[derive(Deserialize, Clone)] pub struct OwnedSpanInfo { pub id: u64, pub name: String, pub start: Duration, pub end: Duration, #[allow(dead_code)] pub parents: Option>, pub is_main_thread: bool, pub fields: Option>, } impl OwnedSpanInfo { fn secs(&self) -> f32 { (self.end - self.start).as_secs_f32() } } /// Common visualization options. #[derive(Debug, Clone)] pub struct PlotConfig { /// Don't overlay bottom spans. pub multi_lane: bool, /// Remove spans shorter than this. pub min_length: Option, /// Remove spans with this name. pub remove: Option>, /// If the is only one field, display its value inline. /// /// Since the text is not limited to its box, text can overlap and become unreadable. pub inline_field: bool, /// The color for the plots in the active region, when running on the main thread. Default: semi-transparent orange pub color_top_blocking: String, /// The color for the plots in the active region, when the work offloaded from the main thread (with /// `tokio::task::spawn_blocking`. Default: semi-transparent green pub color_top_threadpool: String, /// The color for the plots in the total region. Default: semi-transparent blue pub color_bottom: String, } impl Default for PlotConfig { fn default() -> Self { PlotConfig { multi_lane: false, min_length: None, remove: None, inline_field: false, // See http://www.cookbook-r.com/Graphs/Colors_(ggplot2)/#a-colorblind-friendly-palette color_top_blocking: "#E69F0088".to_string(), color_top_threadpool: "#009E7388".to_string(), color_bottom: "#56B4E988".to_string(), } } } /// The dimensions of each part of the plot. #[derive(Debug, Clone)] pub struct PlotLayout { /// Padding top for the entire svg. pub padding_top: usize, /// Padding bottom for the entire svg. pub padding_bottom: usize, /// Padding left for the entire svg. pub padding_left: usize, /// Padding right for the entire svg. pub padding_right: usize, /// The width of the text column on the left. pub text_col_width: usize, /// The of the bar plot section on the entire middle-right. pub content_col_width: usize, /// The height of each of the bars. pub bar_height: usize, /// In expanded mode, this much space is between the tracks. pub multi_lane_padding: usize, /// The padding between different kinds of spans. pub section_padding_height: usize, } impl Default for PlotLayout { fn default() -> Self { PlotLayout { padding_top: 5, padding_bottom: 5, padding_left: 5, padding_right: 5, text_col_width: 250, content_col_width: 850, bar_height: 20, multi_lane_padding: 1, section_padding_height: 10, } } } /// Visualize the spans. /// /// You can store the result with `svg::save(plot_file, &svg)`. pub fn plot( spans: &[OwnedSpanInfo], end: Duration, config: &PlotConfig, layout: &PlotLayout, ) -> SVG { // TODO(konstin): Cow or move out of this method? let spans = if let Some(remove) = &config.remove { spans .iter() .filter(|span| !remove.contains(&span.name)) .cloned() .collect::>() } else { spans.to_vec() }; let mut full_spans: FxHashMap = FxHashMap::default(); for span in &spans { // These are in order because a span is emitted when it exits and exit must happen before // re-entry full_spans.entry(span.id).or_insert(span.clone()).end = span.end; } // Remove to short spans // TODO(konstin): Again, copy on write? let (spans, full_spans) = if let Some(min_length) = config.min_length { let mut removed_ids = HashSet::new(); for (id, full_span) in &full_spans { if full_span.end - full_span.start < min_length { removed_ids.insert(*id); } } let spans = spans .iter() .filter(|span| !removed_ids.contains(&span.id)) .cloned() .collect::>(); for removed_id in removed_ids { full_spans.remove(&removed_id); } (spans, full_spans) } else { (spans.to_vec(), full_spans) }; let mut earliest_starts: FxHashMap<&str, Duration> = FxHashMap::default(); for span in &spans { // For the left sidebar, sort spans by the first time a span name occurred match earliest_starts.entry(&span.name) { Entry::Occupied(mut entry) => { if entry.get() > &span.start { entry.insert(span.start); } } Entry::Vacant(entry) => { entry.insert(span.start); } } } // In expanded mode, we avoid overlaps in different lanes, so we track // until which timestamp each lane is blocked and how many lanes we need. let mut lanes_end: HashMap<&str, Vec> = HashMap::new(); let mut span_lanes = HashMap::new(); let mut full_spans_sorted: Vec<_> = full_spans.values().collect(); full_spans_sorted.sort_by_key(|span| span.start); for full_span in full_spans_sorted { if config.multi_lane { let lanes = lanes_end.entry(&full_span.name).or_default(); if let Some((idx, lane_end)) = lanes .iter_mut() .enumerate() .find(|(_idx, end)| &full_span.start > end) { span_lanes.insert(full_span.id, idx); *lane_end = full_span.end; } else { span_lanes.insert(full_span.id, lanes.len()); lanes.push(full_span.end) } } else { span_lanes.insert(full_span.id, 0); lanes_end .entry(&full_span.name) .or_insert_with(|| vec![full_span.end])[0] = full_span.end; } } let extra_lane_height = layout.bar_height / 2 + layout.multi_lane_padding; let mut earliest_starts: Vec<_> = earliest_starts.into_iter().collect(); earliest_starts.sort_by_key(|(_name, duration)| *duration); let name_offsets: FxHashMap<&str, usize> = earliest_starts .iter() .enumerate() // Add an empty line for the timeline .map(|(idx, (name, _earliest_start))| (*name, idx + 1)) .collect(); // TODO(konstin): Functional version? let mut extra_lanes_cur = 0; let mut extra_lanes_cumulative = HashMap::new(); for (name, _start) in earliest_starts { extra_lanes_cumulative.insert(name, extra_lanes_cur); extra_lanes_cur += lanes_end[name].len() - 1; } let total_width = layout.padding_left + layout.text_col_width + layout.content_col_width + layout.padding_right; // Don't forget the timeline row let total_height = layout.padding_top + (layout.bar_height + layout.section_padding_height) * (name_offsets.len() + 1) + extra_lane_height * extra_lanes_cur + layout.padding_bottom; let mut document = Document::new() .set("width", total_width) .set("height", total_height) .set("viewBox", (0, 0, total_width, total_height)); document = document .add( Text::new("0s") .set("x", layout.text_col_width) .set("y", layout.padding_top + layout.bar_height / 2) .set("dominant-baseline", "middle") .set("text-anchor", "start"), ) .add( Text::new(format!("{:.3}s", end.as_secs_f32())) .set("x", layout.text_col_width + layout.content_col_width) .set("y", layout.padding_top + layout.bar_height / 2) .set("dominant-baseline", "middle") .set("text-anchor", "end"), ); if let Some(min_length) = config.min_length { // Add a note about filtered out spans let text = format!("only spans >{}s", min_length.as_secs_f32()); document = document.add( Text::new(text) .set("x", layout.padding_left) .set("y", layout.padding_top + layout.bar_height / 2) .set("dominant-baseline", "middle") .set("text-anchor", "start"), ); } // Draw the legend on the left for (name, offset) in &name_offsets { document = document.add( Text::new(name.to_string()) .set("x", layout.padding_left) .set( "y", layout.padding_top + layout.bar_height / 2 + offset * (layout.bar_height + layout.section_padding_height) + extra_lane_height * extra_lanes_cumulative[name], ) .set("dominant-baseline", "middle"), ); } let format_tooltip = |span: &OwnedSpanInfo| { let fields = span .fields .iter() .flatten() .map(|(key, value)| format!("{key}: {value}")) .join("\n"); format!("{} {:.3}s\n{}", span.name, span.secs(), fields) }; // Draw the active top half of each span for span in &spans { let offset = name_offsets[span.name.as_str()]; let color = if span.is_main_thread { config.color_top_blocking.clone() } else { config.color_top_threadpool.clone() }; document = document.add( Rectangle::new() .set( "x", layout.text_col_width as f32 + layout.content_col_width as f32 * span.start.as_secs_f32() / end.as_secs_f32(), ) .set( "y", offset * (layout.bar_height + layout.section_padding_height) + extra_lane_height * extra_lanes_cumulative[span.name.as_str()], ) .set( "width", layout.content_col_width as f32 * span.secs() / end.as_secs_f32(), ) .set("height", layout.bar_height / 2) .set("fill", color) // Add tooltip .add(Title::new(format_tooltip(span))), ) } // Draw the total bottom half of each span for full_span in full_spans.values() { let x = layout.text_col_width as f32 + layout.content_col_width as f32 * full_span.start.as_secs_f32() / end.as_secs_f32(); let y = name_offsets[full_span.name.as_str()] * (layout.bar_height + layout.section_padding_height) + extra_lane_height * extra_lanes_cumulative[full_span.name.as_str()] + extra_lane_height * span_lanes[&full_span.id] + layout.bar_height / 2; let width = layout.content_col_width as f32 * (full_span.end - full_span.start).as_secs_f32() / end.as_secs_f32(); let height = layout.bar_height / 2; document = document.add( Rectangle::new() .set("x", x) .set("y", y) .set("width", width) .set("height", height) .set("fill", config.color_bottom.to_string()) // Add tooltip .add(Title::new(format_tooltip(full_span))), ); let mut fields = full_span .fields .as_ref() .map(|map| map.values()) .into_iter() .flatten(); if let Some(value) = fields.next() { if config.inline_field && fields.next().is_none() { document = document.add( Text::new(value) .set("x", x) .set("y", y + height / 2) .set("font-size", "0.7em") .set("dominant-baseline", "middle") .set("text-anchor", "start"), ) } } } document }