├── .github └── workflows │ └── rust.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── examples └── activity_monitor.rs └── src ├── cpu ├── android_linux.rs ├── ios_macos.rs ├── mod.rs └── windows │ ├── mod.rs │ ├── process_times.rs │ ├── system_times.rs │ └── thread_times.rs ├── fd ├── android_linux.rs ├── darwin_private.rs ├── ios.rs ├── macos.rs ├── mod.rs └── windows.rs ├── io └── mod.rs ├── lib.rs ├── mem ├── allocation_counter.rs ├── apple │ ├── heap.rs │ ├── mod.rs │ └── vm.rs ├── mod.rs └── process_memory_info.rs └── utils ├── mod.rs ├── ptr_upgrade.rs └── windows_handle.rs /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | linux-android: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: nightly 17 | default: true 18 | components: rustfmt, clippy 19 | - name: Check code format 20 | run: cargo fmt -- --check 21 | # - name: Check code style # enable clippy in the future 22 | # run: cargo clippy --all-targets -- -Dwarnings 23 | - name: Build 24 | run: cargo build --verbose 25 | - name: Run tests 26 | run: cargo test --lib --verbose 27 | - uses: nttld/setup-ndk@v1 28 | with: 29 | ndk-version: r22b 30 | - name: Build on android 31 | run: rustup target add armv7-linux-androideabi && cargo build --target armv7-linux-androideabi 32 | - name: Build on android 64 33 | run: rustup target add aarch64-linux-android && cargo build --target aarch64-linux-android 34 | macos-ios: 35 | runs-on: macos-latest 36 | steps: 37 | - uses: actions/checkout@v2 38 | - uses: actions-rs/toolchain@v1 39 | with: 40 | toolchain: stable 41 | target: aarch64-apple-ios 42 | - name: Build on MacOS 43 | run: cargo build --verbose 44 | - name: Build for iOS 45 | run: cargo build --target aarch64-apple-ios 46 | windows: 47 | runs-on: windows-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | - uses: actions-rs/toolchain@v1 51 | with: 52 | toolchain: stable 53 | - name: Build on Windows 54 | run: cargo build --verbose 55 | 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "perf_monitor" 3 | version = "0.2.0" 4 | authors = ["zhangli.pear "] 5 | edition = "2018" 6 | 7 | license-file = "LICENSE" 8 | description = "A toolkit designed to be a foundation for applications to monitor their performance." 9 | repository = "https://github.com/larksuite/perf-monitor-rs" 10 | documentation = "https://docs.rs/perf_monitor/" 11 | 12 | categories = ["api-bindings", "accessibility", "development-tools"] 13 | keywords = ["perf", "statistics", "monitor", "performance"] 14 | 15 | 16 | [features] 17 | allocation_counter = [] 18 | darwin_private = [] 19 | 20 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 21 | [dependencies] 22 | libc = "0.2" 23 | thiserror = "1" 24 | 25 | [target.'cfg(target_os = "windows")'.dependencies] 26 | windows-sys = { version = "0.48.0", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_System_ProcessStatus"] } 27 | 28 | [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] 29 | mach = "0.3" 30 | 31 | [build-dependencies] 32 | bindgen = "0.59" 33 | cc = "1.0" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Lark Technologies Pte. Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or 8 | sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | This project uses some sub-components listed below. 25 | For the components licensed under multiple licenses, 26 | we choose to redistribute them under the MIT license. 27 | 28 | ****** 29 | Cc-rs licensed under MIT or Apache-2.0 license 30 | Copyright (c) 2014 Alex Crichton 31 | ****** 32 | rust-bindgen licensed under BSD-3-Clause license 33 | Copyright (c) 2013, Jyun-Yan You 34 | All rights reserved. 35 | ****** 36 | mach licensed under BSD-2-Clause or Apache-2.0 or MIT license. 37 | Copyright (c) 2019 Nick Fitzgerald 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # perf-monitor-rs 2 | 3 | [![github](https://img.shields.io/badge/GitHub-perf_monitor_rs-9b88bb?logo=github)](https://github.com/larksuite/perf-monitor-rs) 4 | [![minimum rustc 1.31.0](https://img.shields.io/badge/Minimum%20rustc-1.31.0-c18170?logo=rust)](https://blog.rust-lang.org/2018/12/06/Rust-1.31-and-rust-2018.html) 5 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 6 | [![docs.rs](https://docs.rs/perf_monitor/badge.svg)](https://docs.rs/perf_monitor) 7 | [![crates.io](https://img.shields.io/crates/v/perf_monitor.svg)](https://crates.io/crates/perf_monitor) 8 | 9 | ```toml 10 | # Cargo.toml 11 | [dependencies] 12 | perf_monitor = "0.2" 13 | ``` 14 | 15 | A toolkit designed to be a foundation for applications to monitor their performance. It is: 16 | - **Cross-Platform:** perf-monitor supports Windows, macOS, Linux, iOS, and Android. 17 | - **Safe Wrapper:** perf-monitor uses many system C interfaces internally but exposes safe wrapper API outside. 18 | - **Effective:** perf-monitor is a thin wrapper around underlying APIs, taking care of not introducing unnecessary overhead, choosing the most lightweight method among many homogeneous APIs. 19 | 20 | # Features 21 | - CPU 22 | - Usage of current process 23 | - Usage of other process (coming soon) 24 | - Usage of any thread in current process 25 | - Logic core number 26 | - Memory 27 | - A global allocator that tracks rust allocations 28 | - Process memory info of current process for Windows and MacOS(Linux is conming soon). 29 | - IO 30 | - Disk IO 31 | - Network IO(coming soon) 32 | - FD 33 | - FD number 34 | 35 | # Example 36 | A simple activity monitor: 37 | 38 | ```rust 39 | use perf_monitor::cpu::{ThreadStat, ProcessStat, processor_numbers}; 40 | use perf_monitor::fd::fd_count_cur; 41 | use perf_monitor::io::get_process_io_stats; 42 | use perf_monitor::mem::get_process_memory_info; 43 | 44 | // cpu 45 | let core_num = processor_numbers().unwrap(); 46 | let mut stat_p = ProcessStat::cur().unwrap(); 47 | let mut stat_t = ThreadStat::cur().unwrap(); 48 | 49 | let _ = (0..1_000).into_iter().sum::(); 50 | 51 | let usage_p = stat_p.cpu().unwrap() * 100f64; 52 | let usage_t = stat_t.cpu().unwrap() * 100f64; 53 | 54 | println!("[CPU] core Number: {}, process usage: {:.2}%, current thread usage: {:.2}%", core_num, usage_p, usage_t); 55 | 56 | // mem 57 | let mem_info = get_process_memory_info().unwrap(); 58 | println!("[Memory] memory used: {} bytes, virtural memory used: {} bytes ", mem_info.resident_set_size, mem_info.virtual_memory_size); 59 | 60 | // fd 61 | let fd_num = fd_count_cur().unwrap(); 62 | println!("[FD] fd number: {}", fd_num); 63 | 64 | // io 65 | let io_stat = get_process_io_stats().unwrap(); 66 | println!("[IO] io-in: {} bytes, io-out: {} bytes", io_stat.read_bytes, io_stat.write_bytes); 67 | ``` 68 | 69 | The above code should have the following output: 70 | ```txt 71 | [CPU] core Number: 12, process usage: 502.16%, current thread usage: 2.91% 72 | [Memory] memory used: 1073152 bytes, virtural memory used: 4405747712 bytes 73 | [FD] fd number: 7 74 | [IO] io-in: 0 bytes, io-out: 32768 bytes 75 | ``` 76 | 77 | See [examples](./examples/activity_monitor.rs) for details. 78 | 79 | # Perfomance 80 | We are concerned about the overhead associated with obtaining performance information. We try to use the most efficient methods while ensuring the API usability. 81 | 82 | For example, CPU usage and FD number cost on these devices has following result: 83 | - MacOS: MacBookPro15,1; 6-Core Intel Core i7; 2.6GHz; 16GB 84 | - Windows: Windows10; Intel Core i3-2310M; 2.10GHz; 64bit; 4GB 85 | - Andorid: Pixel 2; android 10 86 | 87 | | profiling | Windows | MacOS | Android | 88 | | :--- | :---: | :---: | :---: | 89 | | thread CPU usage (ms) | 3 | 0.45 | 16 | 90 | | FD number (ms) | 0.15 | 0.07 | 10 | 91 | 92 | # Supported Platform 93 | 94 | | profiling | Windows | MacOS | iOS | Android | Linux | 95 | | :--- | :---: | :---: | :---: | :---: | :---: | 96 | | [CPU](https://docs.rs/perf_monitor/cpu/index.html) | ✅ | ✅ |✅ |✅ |✅ | 97 | | [Memory](https://docs.rs/perf_monitor/mem/index.html) | ✅ |✅ |✅ |✅ |✅ | 98 | | [FD count](https://docs.rs/perf_monitor/fd/index.html) | ✅ |✅ |❌ |✅ |✅ | 99 | | [IO](https://docs.rs/perf_monitor/io/index.html) | ✅ |✅ |✅ |✅ |✅ 100 | 101 | See [documents](https://docs.rs/perf_monitor/) of each module for usage and more details. 102 | 103 | # Rust Version 104 | 105 | To compile document require the nightly version, others should work both in stable and nightly version. 106 | 107 | 108 | ```shell 109 | cargo build 110 | 111 | cargo +nightly doc 112 | 113 | cargo +nightly test 114 | cargo test --lib 115 | ``` 116 | 117 | # Contribution 118 | 119 | Contributions are welcome! 120 | 121 | Open an issue or create a PR to report bugs, add new features or improve documents and tests. 122 | If you are a new contributor, see [this page](https://github.com/firstcontributions/first-contributions) for help. 123 | 124 | 125 | # Why perf-monitor-rs? 126 | 127 | There are some crates to do similar things, such as [spork](https://github.com/azuqua/spork.rs), [procfs](https://github.com/eminence/procfs), and [sysinfo](https://github.com/GuillaumeGomez/sysinfo). 128 | 129 | Our application needs to monitor itself at runtime to help us find out performance issues. For example, when the CPU usage rises abnormally, we want to figure out which threads cause this. 130 | 131 | However, none of the above crates meet our needs. 132 | 133 | * `spork` can't get other thread information other than the calling thread. Only memory and CPU information can be processed. And it stops updating for years. 134 | * `procfs` looks good enough now, but only support the Linux platform. In its early stages, when we developed perf_monitor_rs, there was no way to get thread information. 135 | * `sysinfo` support all platform we need, but we think its interface is not elegant, because an explicit refresh is required before each call, otherwise an old value will be retrieved and you are not able to tell from the returning value. More importantly, it lacks some features like fd, CPU usage. 136 | 137 | If you are building a cross-platform application and facing the same problem, we hope perf_monitor_rs can be your first choice. 138 | 139 | # License 140 | perf-monitor is providing under the MIT license. See [LICENSE](./LICENSE). 141 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::all, clippy::restriction, clippy::style, clippy::perf)] 2 | use std::{convert::AsRef, env, ffi::OsStr, path::Path, process::Command}; 3 | 4 | /// Using `build.rs` to generate bindings to eliminate the difference 5 | /// among the different target. 6 | /// 7 | /// Following command generate the same output, 8 | /// which makes it easy to glance the bindings when coding. 9 | /// 10 | /// ```shell ignore 11 | /// echo "" > /tmp/bindings.h 12 | /// echo "#include " >> /tmp/bindings.h 13 | /// echo "#include " >> /tmp/bindings.h 14 | /// echo "#include " >> /tmp/bindings.h 15 | /// echo "#include " >> /tmp/bindings.h 16 | /// echo "#include " >> /tmp/bindings.h 17 | /// echo "#include " >> /tmp/bindings.h 18 | /// echo "#include " >> /tmp/bindings.h 19 | /// echo "#include " >> /tmp/bindings.h 20 | /// echo "#include " >> /tmp/bindings.h 21 | /// echo "#include " >> /tmp/bindings.h 22 | /// 23 | /// bindgen \ 24 | /// --with-derive-default \ 25 | /// --with-derive-eq \ 26 | /// --with-derive-ord \ 27 | /// --no-layout-tests \ 28 | /// --whitelist-var THREAD_BASIC_INFO \ 29 | /// --whitelist-var KERN_SUCCESS \ 30 | /// --whitelist-var HOST_BASIC_INFO \ 31 | /// --whitelist-var mach_task_self_ \ 32 | /// --whitelist-var TH_USAGE_SCALE \ 33 | /// --whitelist-type thread_basic_info \ 34 | /// --whitelist-type host_basic_info \ 35 | /// --whitelist-function thread_info \ 36 | /// --whitelist-function mach_thread_self \ 37 | /// --whitelist-function task_threads \ 38 | /// --whitelist-function vm_deallocate \ 39 | /// --whitelist-function host_info \ 40 | /// --whitelist-function mach_host_self \ 41 | /// --whitelist-function pthread_from_mach_thread_np \ 42 | /// --whitelist-function pthread_getname_np \ 43 | /// --whitelist-function task_for_pid \ 44 | /// /tmp/bindings.h > /tmp/bindings.rs 45 | /// ``` 46 | 47 | fn main() { 48 | let target_os = env::var("CARGO_CFG_TARGET_OS"); 49 | if target_os != Ok("macos".into()) && target_os != Ok("ios".into()) { 50 | return; 51 | } 52 | 53 | let target_arch = env::var("CARGO_CFG_TARGET_ARCH"); 54 | 55 | fn build_include_path(sdk: impl AsRef) -> String { 56 | let output = Command::new("xcrun") 57 | .arg("--sdk") 58 | .arg(sdk) 59 | .arg("--show-sdk-path") 60 | .output() 61 | .expect("failed to run xcrun"); 62 | let sdk_path = String::from_utf8(output.stdout.clone()).expect("valid path"); 63 | format!("{}/usr/include", sdk_path.trim()) 64 | } 65 | 66 | let mut include_path = String::new(); 67 | 68 | if target_os == Ok("ios".into()) && target_arch == Ok("aarch64".into()) { 69 | env::set_var("TARGET", "arm64-apple-ios"); 70 | include_path = build_include_path("iphoneos"); 71 | } 72 | 73 | if target_os == Ok("ios".into()) && target_arch == Ok("x86_64".into()) { 74 | env::set_var("TARGET", "x86_64-apple-ios"); 75 | include_path = build_include_path("iphonesimulator"); 76 | } 77 | 78 | if target_os == Ok("macos".into()) { 79 | if target_arch == Ok("x86_64".into()) { 80 | env::set_var("TARGET", "x86_64-apple-darwin"); 81 | include_path = build_include_path("macosx"); 82 | } 83 | if target_arch == Ok("aarch64".into()) { 84 | env::set_var("TARGET", "aarch64-apple-darwin"); 85 | include_path = build_include_path("macosx"); 86 | } 87 | } 88 | 89 | let outdir = env::var("OUT_DIR").expect("OUT_DIR not set"); 90 | let outfile = Path::new(&outdir).join("monitor_rs_ios_macos_binding.rs"); 91 | 92 | bindgen::Builder::default() 93 | .derive_default(true) 94 | .derive_eq(true) 95 | .derive_ord(true) 96 | .layout_tests(false) 97 | // clang args 98 | .clang_arg("-I") 99 | .clang_arg(include_path) 100 | // headers 101 | .header_contents( 102 | "ios_macos.h", 103 | [ 104 | "#include ", 105 | "#include ", 106 | "#include ", 107 | "#include ", 108 | "#include ", 109 | "#include ", 110 | "#include ", 111 | "#include ", 112 | "#include ", 113 | "#include ", 114 | "#include ", 115 | "#include ", 116 | "#include ", 117 | ] 118 | .join("\n") 119 | .as_str(), 120 | ) 121 | // var 122 | .allowlist_var("THREAD_BASIC_INFO") 123 | .allowlist_var("KERN_SUCCESS") 124 | .allowlist_var("HOST_BASIC_INFO") 125 | .allowlist_var("mach_task_self_") 126 | .allowlist_var("TH_USAGE_SCALE") 127 | // type 128 | .allowlist_type("thread_basic_info") 129 | .allowlist_type("host_basic_info") 130 | .allowlist_type("task_vm_info") 131 | .allowlist_type("rusage_info_v2") 132 | .allowlist_type("malloc_zone_t") 133 | // function 134 | .allowlist_function("thread_info") 135 | .allowlist_function("mach_thread_self") 136 | .allowlist_function("task_threads") 137 | .allowlist_function("vm_deallocate") 138 | .allowlist_function("host_info") 139 | .allowlist_function("mach_host_self") 140 | .allowlist_function("pthread_from_mach_thread_np") 141 | .allowlist_function("pthread_getname_np") 142 | .allowlist_function("task_for_pid") 143 | .allowlist_function("malloc_get_all_zones") 144 | .allowlist_function("malloc_default_zone") 145 | // generate 146 | .generate() 147 | .expect("generate binding failed") 148 | .write_to_file(Path::new(&outfile)) 149 | .expect("write to file failed") 150 | } 151 | -------------------------------------------------------------------------------- /examples/activity_monitor.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | use std::time::Instant; 3 | 4 | use perf_monitor::cpu::processor_numbers; 5 | use perf_monitor::cpu::ProcessStat; 6 | use perf_monitor::cpu::ThreadStat; 7 | use perf_monitor::fd::fd_count_cur; 8 | use perf_monitor::io::get_process_io_stats; 9 | use perf_monitor::mem::get_process_memory_info; 10 | 11 | fn main() { 12 | build_some_threads(); 13 | 14 | // cpu 15 | let core_num = processor_numbers().unwrap(); 16 | let mut stat_p = ProcessStat::cur().unwrap(); 17 | let mut stat_t = ThreadStat::cur().unwrap(); 18 | 19 | let mut last_loop = Instant::now(); 20 | loop { 21 | if last_loop.elapsed() > Duration::from_secs(1) { 22 | last_loop = Instant::now(); 23 | } else { 24 | std::thread::sleep(Duration::from_micros(100)); 25 | continue; 26 | } 27 | println!("----------"); 28 | 29 | // cpu 30 | let _ = (0..1_000).into_iter().sum::(); 31 | 32 | let usage_p = stat_p.cpu().unwrap() * 100f64; 33 | let usage_t = stat_t.cpu().unwrap() * 100f64; 34 | 35 | println!( 36 | "[CPU] core Number: {}, process usage: {:.2}%, current thread usage: {:.2}%", 37 | core_num, usage_p, usage_t 38 | ); 39 | 40 | // mem 41 | let mem_info = get_process_memory_info().unwrap(); 42 | 43 | println!( 44 | "[Memory] memory used: {} bytes, virtural memory used: {} bytes ", 45 | mem_info.resident_set_size, mem_info.virtual_memory_size 46 | ); 47 | 48 | // fd 49 | let fd_num = fd_count_cur().unwrap(); 50 | 51 | println!("[FD] fd number: {}", fd_num); 52 | 53 | // io 54 | let io_stat = get_process_io_stats().unwrap(); 55 | 56 | println!( 57 | "[IO] io-in: {} bytes, io-out: {} bytes", 58 | io_stat.read_bytes, io_stat.write_bytes 59 | ); 60 | } 61 | } 62 | 63 | fn build_some_threads() { 64 | for _ in 0..5 { 65 | std::thread::spawn(|| loop { 66 | let _ = (0..9_000).into_iter().sum::(); 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/cpu/android_linux.rs: -------------------------------------------------------------------------------- 1 | use libc::{pthread_t, timespec}; 2 | use std::{ 3 | convert::TryInto, 4 | io::Error, 5 | io::Result, 6 | mem::MaybeUninit, 7 | time::{Duration, Instant}, 8 | }; 9 | 10 | #[derive(Clone, Copy)] 11 | pub struct ThreadId(pthread_t); 12 | 13 | impl ThreadId { 14 | #[inline] 15 | pub fn current() -> Self { 16 | ThreadId(unsafe { libc::pthread_self() }) 17 | } 18 | } 19 | 20 | fn timespec_to_duration(timespec { tv_sec, tv_nsec }: timespec) -> Duration { 21 | let sec: u64 = tv_sec.try_into().unwrap_or_default(); 22 | let nsec: u64 = tv_nsec.try_into().unwrap_or_default(); 23 | let (sec, nanos) = ( 24 | sec.saturating_add(nsec / 1_000_000_000), 25 | (nsec % 1_000_000_000) as u32, 26 | ); 27 | Duration::new(sec, nanos) 28 | } 29 | 30 | fn get_thread_cputime(ThreadId(thread): ThreadId) -> Result { 31 | let mut clk_id = 0; 32 | let ret = unsafe { libc::pthread_getcpuclockid(thread, &mut clk_id) }; 33 | if ret != 0 { 34 | return Err(Error::from_raw_os_error(ret)); 35 | } 36 | 37 | let mut timespec = MaybeUninit::::uninit(); 38 | let ret = unsafe { libc::clock_gettime(clk_id, timespec.as_mut_ptr()) }; 39 | if ret != 0 { 40 | return Err(Error::last_os_error()); 41 | } 42 | Ok(unsafe { timespec.assume_init() }) 43 | } 44 | 45 | pub struct ThreadStat { 46 | tid: ThreadId, 47 | last_stat: (timespec, Instant), 48 | } 49 | 50 | impl ThreadStat { 51 | pub fn cur() -> Result { 52 | Self::build(ThreadId::current()) 53 | } 54 | 55 | pub fn build(tid: ThreadId) -> Result { 56 | let cputime = get_thread_cputime(tid)?; 57 | let total_time = Instant::now(); 58 | Ok(ThreadStat { 59 | tid, 60 | last_stat: (cputime, total_time), 61 | }) 62 | } 63 | 64 | /// un-normalized 65 | pub fn cpu(&mut self) -> Result { 66 | let cputime = get_thread_cputime(self.tid)?; 67 | let total_time = Instant::now(); 68 | let (old_cputime, old_total_time) = 69 | std::mem::replace(&mut self.last_stat, (cputime, total_time)); 70 | let cputime = cputime.tv_sec as f64 + cputime.tv_nsec as f64 / 1_000_000_000f64; 71 | let old_cputime = old_cputime.tv_sec as f64 + old_cputime.tv_nsec as f64 / 1_000_000_000f64; 72 | let dt_cputime = cputime - old_cputime; 73 | let dt_total_time = total_time 74 | .saturating_duration_since(old_total_time) 75 | .as_secs_f64(); 76 | Ok(dt_cputime / dt_total_time) 77 | } 78 | 79 | pub fn cpu_time(&mut self) -> Result { 80 | let cputime = get_thread_cputime(self.tid)?; 81 | let total_time = Instant::now(); 82 | let (old_cputime, _old_total_time) = 83 | std::mem::replace(&mut self.last_stat, (cputime, total_time)); 84 | Ok(timespec_to_duration(cputime).saturating_sub(timespec_to_duration(old_cputime))) 85 | } 86 | } 87 | 88 | pub fn cpu_time() -> Result { 89 | let mut timespec = MaybeUninit::::uninit(); 90 | let ret = unsafe { libc::clock_gettime(libc::CLOCK_PROCESS_CPUTIME_ID, timespec.as_mut_ptr()) }; 91 | if ret != 0 { 92 | return Err(Error::last_os_error()); 93 | } 94 | Ok(timespec_to_duration(unsafe { timespec.assume_init() })) 95 | } 96 | -------------------------------------------------------------------------------- /src/cpu/ios_macos.rs: -------------------------------------------------------------------------------- 1 | use libc::{ 2 | mach_thread_self, rusage, thread_basic_info, time_value_t, KERN_SUCCESS, RUSAGE_SELF, 3 | THREAD_BASIC_INFO, THREAD_BASIC_INFO_COUNT, 4 | }; 5 | use std::convert::TryInto; 6 | use std::mem::MaybeUninit; 7 | use std::time::Instant; 8 | use std::{ 9 | io::{Error, Result}, 10 | time::Duration, 11 | }; 12 | 13 | #[derive(Clone, Copy)] 14 | pub struct ThreadId(u32); 15 | 16 | impl ThreadId { 17 | #[inline] 18 | pub fn current() -> Self { 19 | ThreadId(unsafe { mach_thread_self() }) 20 | } 21 | } 22 | 23 | fn get_thread_basic_info(ThreadId(tid): ThreadId) -> Result { 24 | let mut thread_basic_info = MaybeUninit::::uninit(); 25 | let mut thread_info_cnt = THREAD_BASIC_INFO_COUNT; 26 | 27 | let ret = unsafe { 28 | libc::thread_info( 29 | tid, 30 | THREAD_BASIC_INFO as u32, 31 | thread_basic_info.as_mut_ptr() as *mut _, 32 | &mut thread_info_cnt, 33 | ) 34 | }; 35 | if ret != KERN_SUCCESS as i32 { 36 | return Err(Error::from_raw_os_error(ret)); 37 | } 38 | Ok(unsafe { thread_basic_info.assume_init() }) 39 | } 40 | 41 | pub struct ThreadStat { 42 | tid: ThreadId, 43 | stat: (thread_basic_info, Instant), 44 | } 45 | 46 | impl ThreadStat { 47 | pub fn cur() -> Result { 48 | Self::build(ThreadId::current()) 49 | } 50 | 51 | pub fn build(tid: ThreadId) -> Result { 52 | Ok(ThreadStat { 53 | tid, 54 | stat: (get_thread_basic_info(tid)?, Instant::now()), 55 | }) 56 | } 57 | 58 | /// unnormalized 59 | pub fn cpu(&mut self) -> Result { 60 | let cur_stat = get_thread_basic_info(self.tid)?; 61 | let cur_time = Instant::now(); 62 | let (last_stat, last_time) = std::mem::replace(&mut self.stat, (cur_stat, cur_time)); 63 | 64 | let cur_user_time = time_value_to_u64(cur_stat.user_time); 65 | let cur_sys_time = time_value_to_u64(cur_stat.system_time); 66 | let last_user_time = time_value_to_u64(last_stat.user_time); 67 | let last_sys_time = time_value_to_u64(last_stat.system_time); 68 | 69 | let cpu_time_us = cur_user_time 70 | .saturating_sub(last_user_time) 71 | .saturating_add(cur_sys_time.saturating_sub(last_sys_time)); 72 | 73 | let dt_duration = cur_time.saturating_duration_since(last_time); 74 | Ok(cpu_time_us as f64 / dt_duration.as_micros() as f64) 75 | } 76 | 77 | pub fn cpu_time(&mut self) -> Result { 78 | let cur_stat = get_thread_basic_info(self.tid)?; 79 | let cur_time = Instant::now(); 80 | let (last_stat, _last_time) = std::mem::replace(&mut self.stat, (cur_stat, cur_time)); 81 | 82 | let cur_user_time = time_value_to_u64(cur_stat.user_time); 83 | let cur_sys_time = time_value_to_u64(cur_stat.system_time); 84 | let last_user_time = time_value_to_u64(last_stat.user_time); 85 | let last_sys_time = time_value_to_u64(last_stat.system_time); 86 | 87 | let cpu_time_us = cur_user_time 88 | .saturating_sub(last_user_time) 89 | .saturating_add(cur_sys_time.saturating_sub(last_sys_time)); 90 | 91 | Ok(Duration::from_micros(cpu_time_us)) 92 | } 93 | } 94 | 95 | #[inline] 96 | fn time_value_to_u64(t: time_value_t) -> u64 { 97 | (t.seconds.try_into().unwrap_or(0u64)) 98 | .saturating_mul(1_000_000) 99 | .saturating_add(t.microseconds.try_into().unwrap_or(0u64)) 100 | } 101 | 102 | pub fn cpu_time() -> Result { 103 | let mut time = MaybeUninit::::uninit(); 104 | let ret = unsafe { libc::getrusage(RUSAGE_SELF, time.as_mut_ptr()) }; 105 | if ret != 0 { 106 | return Err(Error::last_os_error()); 107 | } 108 | let time = unsafe { time.assume_init() }; 109 | let sec = (time.ru_utime.tv_sec as u64).saturating_add(time.ru_stime.tv_sec as u64); 110 | let nsec = (time.ru_utime.tv_usec as u32) 111 | .saturating_add(time.ru_stime.tv_usec as u32) 112 | .saturating_mul(1000); 113 | Ok(Duration::new(sec, nsec)) 114 | } 115 | 116 | #[cfg(test)] 117 | #[allow(clippy::all, clippy::print_stdout)] 118 | mod tests { 119 | use super::*; 120 | use test::Bencher; 121 | 122 | // There is a field named `cpu_usage` in `thread_basic_info` which represents the CPU usage of the thread. 123 | // However, we have no idea about how long the interval is. And it will make the API being different from other platforms. 124 | // We calculate the usage instead of using the field directory to make the API is the same on all platforms. 125 | // The cost of the calculation is very very small according to the result of the following benchmark. 126 | #[bench] 127 | fn bench_cpu_usage_by_calculate(b: &mut Bencher) { 128 | let tid = ThreadId::current(); 129 | let last_stat = get_thread_basic_info(tid).unwrap(); 130 | let last_time = Instant::now(); 131 | 132 | b.iter(|| { 133 | let cur_stat = get_thread_basic_info(tid).unwrap(); 134 | let cur_time = Instant::now(); 135 | 136 | let cur_user_time = time_value_to_u64(cur_stat.user_time); 137 | let cur_sys_time = time_value_to_u64(cur_stat.system_time); 138 | let last_user_time = time_value_to_u64(last_stat.user_time); 139 | let last_sys_time = time_value_to_u64(last_stat.system_time); 140 | 141 | let dt_duration = cur_time - last_time; 142 | let cpu_time_us = cur_user_time + cur_sys_time - last_user_time - last_sys_time; 143 | let dt_wtime = Duration::from_micros(cpu_time_us); 144 | 145 | let _ = (cur_stat, cur_time); 146 | let _ = dt_wtime.as_micros() as f64 / dt_duration.as_micros() as f64; 147 | }); 148 | } 149 | 150 | #[bench] 151 | fn bench_cpu_usage_by_field(b: &mut Bencher) { 152 | let tid = ThreadId::current(); 153 | b.iter(|| { 154 | let cur_stat = get_thread_basic_info(tid).unwrap(); 155 | let _ = cur_stat.cpu_usage / 1000; 156 | }); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/cpu/mod.rs: -------------------------------------------------------------------------------- 1 | //! Get cpu usage for current process and specified thread. 2 | //! 3 | //! A method named `cpu` on `ThreadStat` and `ProcessStat` 4 | //! can retrieve cpu usage of thread and process respectively. 5 | //! 6 | //! The returning value is unnormalized, that is for multi-processor machine, 7 | //! the cpu usage will beyond 100%, for example returning 2.8 means 280% cpu usage. 8 | //! If normalized value is what you expected, divide the returning by processor_numbers. 9 | //! 10 | //! ## Example 11 | //! 12 | //! ``` 13 | //! # use perf_monitor::cpu::ThreadStat; 14 | //! let mut stat = ThreadStat::cur().unwrap(); 15 | //! let _ = (0..1_000_000).into_iter().sum::(); 16 | //! let usage = stat.cpu().unwrap(); 17 | //! println!("current thread cpu usage is {:.2}%", usage * 100f64); 18 | //! ``` 19 | //! 20 | //! ## Bottom Layer Interface 21 | //! | platform | thread | process | 22 | //! | -- | -- | -- | 23 | //! | windows |[GetThreadTimes] | [GetProcessTimes] | 24 | //! | linux & android | [/proc/{pid}/task/{tid}/stat][man5] | [clockgettime] | 25 | //! | macos & ios | [thread_info] | [getrusage] | 26 | //! 27 | //! [GetThreadTimes]: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadtimes 28 | //! [GetProcessTimes]: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesstimes 29 | //! [man5]: https://man7.org/linux/man-pages/man5/proc.5.html 30 | //! [thread_info]: http://web.mit.edu/darwin/src/modules/xnu/osfmk/man/thread_info.html 31 | //! [clockgettime]: https://man7.org/linux/man-pages/man2/clock_gettime.2.html 32 | //! [getrusage]: https://www.man7.org/linux/man-pages/man2/getrusage.2.html 33 | 34 | #[cfg(any(target_os = "linux", target_os = "android"))] 35 | mod android_linux; 36 | #[cfg(any(target_os = "ios", target_os = "macos"))] 37 | mod ios_macos; 38 | #[cfg(target_os = "windows")] 39 | mod windows; 40 | 41 | #[cfg(any(target_os = "linux", target_os = "android"))] 42 | use android_linux as platform; 43 | #[cfg(any(target_os = "ios", target_os = "macos"))] 44 | use ios_macos as platform; 45 | #[cfg(target_os = "windows")] 46 | use windows as platform; 47 | 48 | pub use platform::{cpu_time, ThreadId}; 49 | pub use std::io::Result; 50 | use std::{ 51 | io, mem, 52 | time::{Duration, Instant}, 53 | }; 54 | 55 | /// logical processor number 56 | pub fn processor_numbers() -> std::io::Result { 57 | std::thread::available_parallelism().map(|x| x.get()) 58 | } 59 | 60 | /// A struct to monitor process cpu usage 61 | pub struct ProcessStat { 62 | now: Instant, 63 | cpu_time: Duration, 64 | } 65 | 66 | impl ProcessStat { 67 | /// return a monitor of current process 68 | pub fn cur() -> io::Result { 69 | Ok(ProcessStat { 70 | now: Instant::now(), 71 | cpu_time: platform::cpu_time()?, 72 | }) 73 | } 74 | 75 | /// return the cpu usage from last invoke, 76 | /// or when this struct created if it is the first invoke. 77 | pub fn cpu(&mut self) -> io::Result { 78 | let old_time = mem::replace(&mut self.cpu_time, platform::cpu_time()?); 79 | let old_now = mem::replace(&mut self.now, Instant::now()); 80 | let real_time = self.now.saturating_duration_since(old_now).as_secs_f64(); 81 | let cpu_time = self.cpu_time.saturating_sub(old_time).as_secs_f64(); 82 | Ok(cpu_time / real_time) 83 | } 84 | } 85 | 86 | /// A struct to monitor thread cpu usage 87 | pub struct ThreadStat { 88 | stat: platform::ThreadStat, 89 | } 90 | 91 | impl ThreadStat { 92 | /// return a monitor of current thread. 93 | pub fn cur() -> Result { 94 | Ok(ThreadStat { 95 | stat: platform::ThreadStat::cur()?, 96 | }) 97 | } 98 | 99 | /// return a monitor of specified thread. 100 | /// 101 | /// `tid` is **NOT** `std::thread::ThreadId`. 102 | /// [`ThreadId::current`] can be used to retrieve a valid tid. 103 | pub fn build(thread_id: ThreadId) -> Result { 104 | Ok(ThreadStat { 105 | stat: platform::ThreadStat::build(thread_id)?, 106 | }) 107 | } 108 | 109 | /// return the cpu usage from last invoke, 110 | /// or when this struct created if it is the first invoke. 111 | pub fn cpu(&mut self) -> Result { 112 | self.stat.cpu() 113 | } 114 | 115 | /// return the cpu_time in user mode and system mode from last invoke, 116 | /// or when this struct created if it is the first invoke. 117 | pub fn cpu_time(&mut self) -> Result { 118 | self.stat.cpu_time() 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod test { 124 | use super::*; 125 | 126 | // this test should be executed alone. 127 | #[test] 128 | #[ignore] 129 | fn test_process_usage() { 130 | let mut stat = ProcessStat::cur().unwrap(); 131 | 132 | std::thread::sleep(std::time::Duration::from_secs(1)); 133 | 134 | let usage = stat.cpu().unwrap(); 135 | 136 | assert!(usage < 0.01); 137 | 138 | let num = processor_numbers().unwrap(); 139 | for _ in 0..num * 10 { 140 | std::thread::spawn(move || loop { 141 | let _ = (0..10_000_000).into_iter().sum::(); 142 | }); 143 | } 144 | 145 | let mut stat = ProcessStat::cur().unwrap(); 146 | 147 | std::thread::sleep(std::time::Duration::from_secs(1)); 148 | 149 | let usage = stat.cpu().unwrap(); 150 | 151 | assert!(usage > 0.9 * num as f64) 152 | } 153 | 154 | #[test] 155 | fn test_thread_usage() { 156 | let mut stat = ThreadStat::cur().unwrap(); 157 | 158 | std::thread::sleep(std::time::Duration::from_secs(1)); 159 | let usage = stat.cpu().unwrap(); 160 | assert!(usage < 0.01); 161 | 162 | let mut x = 1_000_000u64; 163 | std::hint::black_box(&mut x); 164 | let mut times = 1000u64; 165 | std::hint::black_box(&mut times); 166 | for i in 0..times { 167 | let x = (0..x + i).into_iter().sum::(); 168 | std::hint::black_box(x); 169 | } 170 | let usage = stat.cpu().unwrap(); 171 | assert!(usage > 0.5) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/cpu/windows/mod.rs: -------------------------------------------------------------------------------- 1 | use super::processor_numbers; 2 | use super::windows::process_times::ProcessTimes; 3 | use super::windows::system_times::SystemTimes; 4 | use super::windows::thread_times::ThreadTimes; 5 | use std::io::Result; 6 | use std::time::Duration; 7 | use windows_sys::Win32::Foundation::FILETIME; 8 | use windows_sys::Win32::System::Threading::GetCurrentThreadId; 9 | 10 | pub mod process_times; 11 | pub mod system_times; 12 | pub mod thread_times; 13 | 14 | #[derive(Clone, Copy)] 15 | pub struct ThreadId(u32); 16 | 17 | impl ThreadId { 18 | #[inline] 19 | pub fn current() -> Self { 20 | ThreadId(unsafe { GetCurrentThreadId() }) 21 | } 22 | } 23 | 24 | /// convert to u64, unit 100 ns 25 | fn filetime_to_ns100(ft: &FILETIME) -> u64 { 26 | ((ft.dwHighDateTime as u64) << 32) + ft.dwLowDateTime as u64 27 | } 28 | 29 | pub struct ThreadStat { 30 | tid: ThreadId, 31 | last_work_time: u64, 32 | last_total_time: u64, 33 | } 34 | 35 | impl ThreadStat { 36 | fn get_times(thread_id: ThreadId) -> Result<(u64, u64)> { 37 | let system_times = SystemTimes::capture()?; 38 | let thread_times = ThreadTimes::capture_with_thread_id(thread_id)?; 39 | 40 | let work_time = 41 | filetime_to_ns100(&thread_times.kernel) + filetime_to_ns100(&thread_times.user); 42 | let total_time = 43 | filetime_to_ns100(&system_times.kernel) + filetime_to_ns100(&system_times.user); 44 | Ok((work_time, total_time)) 45 | } 46 | 47 | pub fn cur() -> Result { 48 | let tid = ThreadId::current(); 49 | let (work_time, total_time) = Self::get_times(tid)?; 50 | Ok(ThreadStat { 51 | tid, 52 | last_work_time: work_time, 53 | last_total_time: total_time, 54 | }) 55 | } 56 | 57 | pub fn build(tid: ThreadId) -> Result { 58 | let (work_time, total_time) = Self::get_times(tid)?; 59 | Ok(ThreadStat { 60 | tid, 61 | last_work_time: work_time, 62 | last_total_time: total_time, 63 | }) 64 | } 65 | 66 | pub fn cpu(&mut self) -> Result { 67 | let (work_time, total_time) = Self::get_times(self.tid)?; 68 | 69 | let dt_total_time = total_time - self.last_total_time; 70 | if dt_total_time == 0 { 71 | return Ok(0.0); 72 | } 73 | let dt_work_time = work_time - self.last_work_time; 74 | 75 | self.last_work_time = work_time; 76 | self.last_total_time = total_time; 77 | 78 | Ok(dt_work_time as f64 / dt_total_time as f64 * processor_numbers()? as f64) 79 | } 80 | 81 | pub fn cpu_time(&mut self) -> Result { 82 | let (work_time, total_time) = Self::get_times(self.tid)?; 83 | 84 | let cpu_time = work_time - self.last_work_time; 85 | 86 | self.last_work_time = work_time; 87 | self.last_total_time = total_time; 88 | 89 | Ok(Duration::from_nanos(cpu_time)) 90 | } 91 | } 92 | 93 | #[inline] 94 | pub fn cpu_time() -> Result { 95 | let process_times = ProcessTimes::capture_current()?; 96 | 97 | let kt = filetime_to_ns100(&process_times.kernel); 98 | let ut = filetime_to_ns100(&process_times.user); 99 | 100 | // convert ns 101 | // 102 | // Note: make it ns unit may overflow in some cases. 103 | // For example, a machine with 128 cores runs for one year. 104 | let cpu = (kt + ut).saturating_mul(100); 105 | 106 | // make it un-normalized 107 | let cpu = cpu * processor_numbers()? as u64; 108 | 109 | Ok(Duration::from_nanos(cpu)) 110 | } 111 | -------------------------------------------------------------------------------- /src/cpu/windows/process_times.rs: -------------------------------------------------------------------------------- 1 | use std::io::Error; 2 | use std::io::Result; 3 | use std::mem::MaybeUninit; 4 | use windows_sys::Win32::Foundation::FILETIME; 5 | use windows_sys::Win32::Foundation::HANDLE; 6 | use windows_sys::Win32::System::Threading::GetCurrentProcess; 7 | use windows_sys::Win32::System::Threading::GetProcessTimes; 8 | 9 | pub struct ProcessTimes { 10 | pub create: FILETIME, 11 | pub exit: FILETIME, 12 | pub kernel: FILETIME, 13 | pub user: FILETIME, 14 | } 15 | 16 | impl ProcessTimes { 17 | pub fn capture_current() -> Result { 18 | unsafe { Self::capture_with_handle(GetCurrentProcess()) } 19 | } 20 | 21 | pub unsafe fn capture_with_handle(handle: HANDLE) -> Result { 22 | let mut create = MaybeUninit::::uninit(); 23 | let mut exit = MaybeUninit::::uninit(); 24 | let mut kernel = MaybeUninit::::uninit(); 25 | let mut user = MaybeUninit::::uninit(); 26 | let ret = unsafe { 27 | GetProcessTimes( 28 | handle, 29 | create.as_mut_ptr(), 30 | exit.as_mut_ptr(), 31 | kernel.as_mut_ptr(), 32 | user.as_mut_ptr(), 33 | ) 34 | }; 35 | if ret == 0 { 36 | return Err(Error::last_os_error()); 37 | } 38 | Ok(unsafe { 39 | Self { 40 | create: create.assume_init(), 41 | exit: exit.assume_init(), 42 | kernel: kernel.assume_init(), 43 | user: user.assume_init(), 44 | } 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/cpu/windows/system_times.rs: -------------------------------------------------------------------------------- 1 | use std::io::Error; 2 | use std::io::Result; 3 | use std::mem::MaybeUninit; 4 | use windows_sys::Win32::Foundation::FILETIME; 5 | use windows_sys::Win32::System::Threading::GetSystemTimes; 6 | 7 | pub struct SystemTimes { 8 | pub idle: FILETIME, 9 | pub kernel: FILETIME, 10 | pub user: FILETIME, 11 | } 12 | 13 | impl SystemTimes { 14 | pub fn capture() -> Result { 15 | let mut idle = MaybeUninit::::uninit(); 16 | let mut kernel = MaybeUninit::::uninit(); 17 | let mut user = MaybeUninit::::uninit(); 18 | let ret = 19 | unsafe { GetSystemTimes(idle.as_mut_ptr(), kernel.as_mut_ptr(), user.as_mut_ptr()) }; 20 | if ret == 0 { 21 | return Err(Error::last_os_error()); 22 | } 23 | Ok(unsafe { 24 | Self { 25 | idle: idle.assume_init(), 26 | kernel: kernel.assume_init(), 27 | user: user.assume_init(), 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/cpu/windows/thread_times.rs: -------------------------------------------------------------------------------- 1 | use super::ThreadId; 2 | use crate::utils::ptr_upgrade::HandleUpgrade; 3 | use crate::utils::windows_handle::Handle; 4 | use std::io::Error; 5 | use std::io::Result; 6 | use std::mem::MaybeUninit; 7 | use windows_sys::Win32::Foundation::FALSE; 8 | use windows_sys::Win32::Foundation::FILETIME; 9 | use windows_sys::Win32::Foundation::HANDLE; 10 | use windows_sys::Win32::System::Threading::GetCurrentThread; 11 | use windows_sys::Win32::System::Threading::GetThreadTimes; 12 | use windows_sys::Win32::System::Threading::OpenThread; 13 | use windows_sys::Win32::System::Threading::THREAD_QUERY_LIMITED_INFORMATION; 14 | 15 | pub struct ThreadTimes { 16 | pub create: FILETIME, 17 | pub exit: FILETIME, 18 | pub kernel: FILETIME, 19 | pub user: FILETIME, 20 | } 21 | 22 | impl ThreadTimes { 23 | #[allow(dead_code)] 24 | pub fn capture_current() -> Result { 25 | unsafe { Self::capture_with_handle(GetCurrentThread()) } 26 | } 27 | 28 | pub fn capture_with_thread_id(ThreadId(thread_id): ThreadId) -> Result { 29 | // Use THREAD_QUERY_LIMITED_INFORMATION to acquire minimum access rights and 30 | // support for Windows Server 2023 and Windows XP is dropped: 31 | // 32 | // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadtimes 33 | let handle = 34 | unsafe { OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE as i32, thread_id) } 35 | .upgrade() 36 | .map(|x| unsafe { Handle::new(x) }); 37 | let Some(handle) = handle else { 38 | return Err(Error::last_os_error()); 39 | }; 40 | unsafe { Self::capture_with_handle(handle.as_handle()) } 41 | } 42 | 43 | /// Get thread times for given thread handle, given handle needs specific access rights: 44 | /// 45 | /// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadtimes 46 | pub unsafe fn capture_with_handle(handle: HANDLE) -> Result { 47 | let mut create = MaybeUninit::::uninit(); 48 | let mut exit = MaybeUninit::::uninit(); 49 | let mut kernel = MaybeUninit::::uninit(); 50 | let mut user = MaybeUninit::::uninit(); 51 | let ret = unsafe { 52 | GetThreadTimes( 53 | handle, 54 | create.as_mut_ptr(), 55 | exit.as_mut_ptr(), 56 | kernel.as_mut_ptr(), 57 | user.as_mut_ptr(), 58 | ) 59 | }; 60 | if ret == 0 { 61 | return Err(Error::last_os_error()); 62 | } 63 | Ok(unsafe { 64 | Self { 65 | create: create.assume_init(), 66 | exit: exit.assume_init(), 67 | kernel: kernel.assume_init(), 68 | user: user.assume_init(), 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/fd/android_linux.rs: -------------------------------------------------------------------------------- 1 | pub fn fd_count_pid(pid: u32) -> std::io::Result { 2 | // Subtract 2 to exclude `.`, `..` entries 3 | std::fs::read_dir(format!("/proc/{}/fd", pid)).map(|entries| entries.count().saturating_sub(2)) 4 | } 5 | 6 | pub fn fd_count_cur() -> std::io::Result { 7 | // Subtract 3 to exclude `.`, `..` entries and fd created by `read_dir` 8 | std::fs::read_dir("/proc/self/fd").map(|entries| entries.count().saturating_sub(3)) 9 | } 10 | 11 | #[cfg(test)] 12 | mod test { 13 | use super::*; 14 | 15 | #[test] 16 | fn test_fd_count() { 17 | #[cfg(target_os = "linux")] 18 | const TEMP_DIR: &str = "/tmp"; 19 | #[cfg(target_os = "android")] 20 | const TEMP_DIR: &str = "/data/local/tmp"; 21 | 22 | const NUM: usize = 100; 23 | 24 | // open some files and do not close them. 25 | let fds: Vec<_> = (0..NUM) 26 | .map(|i| { 27 | let fname = format!("{}/tmpfile{}", TEMP_DIR, i); 28 | std::fs::File::create(fname).unwrap() 29 | }) 30 | .collect(); 31 | let count = fd_count_cur().unwrap(); 32 | 33 | dbg!(count); 34 | assert!(count >= NUM); 35 | let old_count = count; 36 | 37 | drop(fds); 38 | let count = fd_count_cur().unwrap(); 39 | // Though tests are run in multi-thread mode without using nextest, we 40 | // assume NUM is big enough to make fd count lower in a short period. 41 | assert!(count < old_count); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/fd/darwin_private.rs: -------------------------------------------------------------------------------- 1 | //! This is assumed to be private api due to App Store's rejection: https://github.com/dotnet/maui/issues/3290 2 | //! 3 | //! Inspired by lsof: 4 | 5 | use libc::proc_taskallinfo; 6 | use std::mem::MaybeUninit; 7 | use std::os::raw::c_int; 8 | use std::os::raw::c_void; 9 | 10 | const PROC_PIDLISTFDS: c_int = 1; 11 | const PROC_PIDTASKALLINFO: c_int = 2; 12 | 13 | extern "C" { 14 | fn proc_pidinfo( 15 | pid: c_int, 16 | flavor: c_int, 17 | arg: u64, 18 | buffer: *mut c_void, 19 | buffersize: c_int, 20 | ) -> c_int; 21 | } 22 | 23 | #[repr(C)] 24 | pub struct proc_fdinfo { 25 | pub proc_fd: i32, 26 | pub proc_fdtype: u32, 27 | } 28 | 29 | pub fn fd_count_cur() -> std::io::Result { 30 | fd_count_pid(std::process::id()) 31 | } 32 | 33 | pub fn fd_count_pid(pid: u32) -> std::io::Result { 34 | let pid = pid as i32; 35 | let max_fds = unsafe { 36 | let mut info = MaybeUninit::::uninit(); 37 | let buffersize = std::mem::size_of::() as c_int; 38 | let ret = proc_pidinfo( 39 | pid, 40 | PROC_PIDTASKALLINFO, 41 | 0, 42 | info.as_mut_ptr() as _, 43 | buffersize, 44 | ); 45 | if ret <= 0 { 46 | return Err(std::io::Error::from_raw_os_error(ret)); 47 | } 48 | if ret < buffersize { 49 | return Err(std::io::Error::new( 50 | std::io::ErrorKind::Other, 51 | "proc_pidinfo(PROC_PIDTASKALLINFO) too few bytes", 52 | )); 53 | } 54 | info.assume_init_ref().pbsd.pbi_nfiles as c_int 55 | }; 56 | let buffersize = max_fds * std::mem::size_of::() as c_int; 57 | let mut buffer = vec![0u8; buffersize as usize]; 58 | let ret = unsafe { 59 | proc_pidinfo( 60 | pid, 61 | PROC_PIDLISTFDS, 62 | 0, 63 | buffer.as_mut_ptr() as _, 64 | buffersize, 65 | ) 66 | }; 67 | if ret <= 0 { 68 | Err(std::io::Error::from_raw_os_error(ret)) 69 | } else { 70 | Ok(ret as usize / std::mem::size_of::()) 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod test { 76 | use super::*; 77 | 78 | #[test] 79 | fn test_fd_count_private() { 80 | // case1: open some files and do not close them. 81 | { 82 | let mut buf = vec![]; 83 | const NUM: usize = 100; 84 | let init_count = fd_count_cur().unwrap(); 85 | 86 | for i in 0..NUM { 87 | let fname = format!("/tmp/fd_count_test_tmpfile{}", i); 88 | let file = std::fs::OpenOptions::new() 89 | .write(true) 90 | .create(true) 91 | .open(fname); 92 | buf.push(file); 93 | } 94 | let count = fd_count_cur().unwrap(); 95 | assert_eq!(NUM + init_count, count); 96 | } 97 | 98 | // case2: compare the result with lsof. 99 | { 100 | let count_devfd = fd_count_cur().unwrap(); 101 | let count_lsof = fd_lsof() - 2; // minus pipe fd between parent process and child process. 102 | assert_eq!(count_lsof, count_devfd); 103 | } 104 | } 105 | 106 | fn fd_lsof() -> usize { 107 | let pid = unsafe { libc::getpid() }; 108 | let output = std::process::Command::new("lsof") 109 | .arg("-p") 110 | .arg(pid.to_string()) 111 | .output() 112 | .unwrap(); 113 | std::thread::sleep(std::time::Duration::from_secs(1)); 114 | let output_txt = String::from_utf8(output.stdout).unwrap(); 115 | let count_lsof = output_txt 116 | .lines() 117 | .filter(|s| s.find("cwd").is_none() && s.find("txt").is_none()) 118 | .map(|s| println!("{}", s)) 119 | .count(); 120 | 121 | count_lsof - 1 // minus title line of lsof output. 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/fd/ios.rs: -------------------------------------------------------------------------------- 1 | // There is no api to retrieve the fd count of the process for ios. 2 | // following links contains a available way, but it's complicated and 3 | // inefficient. 4 | 5 | pub fn fd_count_cur() -> std::io::Result { 6 | unimplemented!() 7 | } 8 | 9 | pub fn fd_count_pid(pid: u32) -> std::io::Result { 10 | unimplemented!() 11 | } 12 | -------------------------------------------------------------------------------- /src/fd/macos.rs: -------------------------------------------------------------------------------- 1 | pub fn fd_count_cur() -> std::io::Result { 2 | // Remove the opening fd created by `read_dir` 3 | std::fs::read_dir("/dev/fd").map(|entries| entries.count().saturating_sub(1)) 4 | } 5 | 6 | #[cfg(test)] 7 | mod test { 8 | use super::*; 9 | 10 | // We put these test case in one test to make them get executed one by one. 11 | // Parallel execution causes them to interact with each other and fail the test. 12 | #[test] 13 | fn test_fd_count() { 14 | // case1: open some files and do not close them. 15 | { 16 | let mut buf = vec![]; 17 | const NUM: usize = 100; 18 | let init_count = fd_count_cur().unwrap(); 19 | 20 | for i in 0..NUM { 21 | let fname = format!("/tmp/fd_count_test_tmpfile{}", i); 22 | let file = std::fs::OpenOptions::new() 23 | .write(true) 24 | .create(true) 25 | .open(fname); 26 | buf.push(file); 27 | } 28 | let count = fd_count_cur().unwrap(); 29 | assert_eq!(NUM + init_count, count); 30 | } 31 | 32 | // case2: compare the result with lsof. 33 | { 34 | let count_devfd = fd_count_cur().unwrap(); 35 | let count_lsof = fd_lsof() - 2; // minus pipe fd between parent process and child process. 36 | assert_eq!(count_lsof, count_devfd); 37 | } 38 | } 39 | 40 | fn fd_lsof() -> usize { 41 | let pid = unsafe { libc::getpid() }; 42 | let output = std::process::Command::new("lsof") 43 | .arg("-p") 44 | .arg(pid.to_string()) 45 | .output() 46 | .unwrap(); 47 | std::thread::sleep(std::time::Duration::from_secs(1)); 48 | let output_txt = String::from_utf8(output.stdout).unwrap(); 49 | let count_lsof = output_txt 50 | .lines() 51 | .filter(|s| s.find("cwd").is_none() && s.find("txt").is_none()) 52 | .map(|s| println!("{}", s)) 53 | .count(); 54 | 55 | count_lsof - 1 // minus title line of lsof output. 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/fd/mod.rs: -------------------------------------------------------------------------------- 1 | //! Get file descriptor(say handle for windows) numbers for current process. 2 | //! 3 | //! ``` 4 | //! use perf_monitor::fd::fd_count_cur; 5 | //! 6 | //! let count = fd_count_cur().unwrap(); 7 | //! ``` 8 | //! 9 | //! ## Bottom Layer Interface 10 | //! 11 | //! - Windows: [GetProcessHandleCount](https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesshandlecount) 12 | //! - Linux & android: [/proc/{pid}/fd](https://man7.org/linux/man-pages/man5/proc.5.html) 13 | //! - MacOS: [/dev/fd](https://www.informit.com/articles/article.aspx?p=99706&seqNum=15) 14 | //! - iOS Unfortunately there is no api to retrieve the fd count of the process for iOS. 15 | //! Following links contains a available method, but it's complicated and 16 | //! inefficient. 17 | //! 18 | //! ## Other Process 19 | //! 20 | //! For windows, linux and android(maybe), it is possible to get fd number of other process. 21 | //! However we didn't re-export these function because macos and ios is not supported. 22 | //! 23 | 24 | #[cfg(target_os = "windows")] 25 | mod windows; 26 | #[cfg(target_os = "windows")] 27 | use windows as platform; 28 | 29 | #[cfg(any(target_os = "linux", target_os = "android"))] 30 | mod android_linux; 31 | #[cfg(any(target_os = "linux", target_os = "android"))] 32 | use android_linux as platform; 33 | 34 | #[cfg(all(target_os = "macos", not(feature = "darwin_private")))] 35 | mod macos; 36 | #[cfg(all(target_os = "macos", not(feature = "darwin_private")))] 37 | use macos as platform; 38 | 39 | #[cfg(all(target_os = "ios", not(feature = "darwin_private")))] 40 | mod ios; 41 | #[cfg(all(target_os = "ios", not(feature = "darwin_private")))] 42 | use ios as platform; 43 | 44 | #[cfg(all( 45 | any(target_os = "macos", target_os = "ios"), 46 | feature = "darwin_private" 47 | ))] 48 | mod darwin_private; 49 | #[cfg(all( 50 | any(target_os = "macos", target_os = "ios"), 51 | feature = "darwin_private" 52 | ))] 53 | use darwin_private as platform; 54 | 55 | /// return the fd count of current process 56 | #[inline] 57 | pub fn fd_count_cur() -> std::io::Result { 58 | platform::fd_count_cur().map(|count| count as usize) 59 | } 60 | -------------------------------------------------------------------------------- /src/fd/windows.rs: -------------------------------------------------------------------------------- 1 | use windows_sys::Win32::Foundation::FALSE; 2 | use windows_sys::Win32::Foundation::HANDLE; 3 | use windows_sys::Win32::System::Threading::GetCurrentProcess; 4 | use windows_sys::Win32::System::Threading::GetProcessHandleCount; 5 | use windows_sys::Win32::System::Threading::OpenProcess; 6 | use windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION; 7 | 8 | use crate::utils::ptr_upgrade::HandleUpgrade; 9 | use crate::utils::windows_handle::Handle; 10 | 11 | #[inline] 12 | fn process_fd_count(handler: HANDLE) -> std::io::Result { 13 | let mut count = 0; 14 | let ret = unsafe { GetProcessHandleCount(handler, &mut count) }; 15 | if ret == 0 { 16 | return Err(std::io::Error::last_os_error()); 17 | } 18 | Ok(count) 19 | } 20 | 21 | pub fn fd_count_pid(pid: u32) -> std::io::Result { 22 | // Use PROCESS_QUERY_LIMITED_INFORMATION to acquire less privilege and drop 23 | // support for Windows Server 2023 and Windows XP: 24 | // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getprocesshandlecount 25 | let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE as i32, pid) } 26 | .upgrade() 27 | .map(|x| unsafe { Handle::new(x) }); 28 | let Some(handle) = handle else { 29 | return Err(std::io::Error::last_os_error()); 30 | }; 31 | process_fd_count(handle.as_handle()) 32 | } 33 | 34 | pub fn fd_count_cur() -> std::io::Result { 35 | process_fd_count(unsafe { GetCurrentProcess() }) 36 | } 37 | 38 | #[cfg(test)] 39 | mod test { 40 | use super::*; 41 | use windows_sys::Win32::Foundation::CloseHandle; 42 | 43 | #[test] 44 | fn test_count_fd() { 45 | const NUM: u32 = 100000; 46 | 47 | // open then close handle 48 | for _ in 0..NUM { 49 | let pid = std::process::id(); 50 | let handler = 51 | unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE as i32, pid) }; 52 | unsafe { CloseHandle(handler) }; 53 | } 54 | let new_count = fd_count_cur().unwrap(); 55 | 56 | assert!(new_count < NUM); 57 | 58 | // open some handle and do not close them 59 | for _ in 0..NUM { 60 | let pid = std::process::id(); 61 | unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE as i32, pid) }; 62 | } 63 | let new_count = fd_count_cur().unwrap(); 64 | 65 | assert!(new_count >= NUM); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/io/mod.rs: -------------------------------------------------------------------------------- 1 | //! Get io usage for current process. 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | #[error("IOStatsError({code}):{msg}")] 6 | pub struct IOStatsError { 7 | pub code: i32, 8 | pub msg: String, 9 | } 10 | 11 | impl From for IOStatsError { 12 | fn from(e: std::io::Error) -> Self { 13 | Self { 14 | code: e.kind() as i32, 15 | msg: e.to_string(), 16 | } 17 | } 18 | } 19 | 20 | impl From for IOStatsError { 21 | fn from(e: std::num::ParseIntError) -> Self { 22 | Self { 23 | code: 0, 24 | msg: e.to_string(), 25 | } 26 | } 27 | } 28 | /// A struct represents io status. 29 | #[derive(Debug, Clone, Default)] 30 | pub struct IOStats { 31 | /// (linux & windows) the number of read operations performed (cumulative) 32 | pub read_count: u64, 33 | 34 | /// (linux & windows) the number of write operations performed (cumulative) 35 | pub write_count: u64, 36 | 37 | /// the number of bytes read (cumulative). 38 | pub read_bytes: u64, 39 | 40 | /// the number of bytes written (cumulative) 41 | pub write_bytes: u64, 42 | } 43 | /// Get the io stats of current process. Most platforms are supported. 44 | #[cfg(any( 45 | target_os = "linux", 46 | target_os = "android", 47 | target_os = "macos", 48 | target_os = "windows" 49 | ))] 50 | pub fn get_process_io_stats() -> Result { 51 | get_process_io_stats_impl() 52 | } 53 | 54 | #[cfg(any(target_os = "linux", target_os = "android"))] 55 | fn get_process_io_stats_impl() -> Result { 56 | use std::{ 57 | io::{BufRead, BufReader}, 58 | str::FromStr, 59 | }; 60 | let mut io_stats = IOStats::default(); 61 | let reader = BufReader::new(std::fs::File::open("/proc/self/io")?); 62 | 63 | for line in reader.lines() { 64 | let line = line?; 65 | let mut s = line.split_whitespace(); 66 | if let (Some(field), Some(value)) = (s.next(), s.next()) { 67 | match field { 68 | "syscr:" => io_stats.read_count = u64::from_str(value)?, 69 | "syscw:" => io_stats.write_count = u64::from_str(value)?, 70 | "read_bytes:" => io_stats.read_bytes = u64::from_str(value)?, 71 | "write_bytes:" => io_stats.write_bytes = u64::from_str(value)?, 72 | _ => continue, 73 | } 74 | } 75 | } 76 | 77 | Ok(io_stats) 78 | } 79 | 80 | #[cfg(target_os = "windows")] 81 | fn get_process_io_stats_impl() -> Result { 82 | use std::mem::MaybeUninit; 83 | use windows_sys::Win32::System::Threading::GetCurrentProcess; 84 | use windows_sys::Win32::System::Threading::GetProcessIoCounters; 85 | use windows_sys::Win32::System::Threading::IO_COUNTERS; 86 | let mut io_counters = MaybeUninit::::uninit(); 87 | let ret = unsafe { 88 | // If the function succeeds, the return value is nonzero. 89 | // If the function fails, the return value is zero. 90 | // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getprocessiocounters 91 | GetProcessIoCounters(GetCurrentProcess(), io_counters.as_mut_ptr()) 92 | }; 93 | if ret == 0 { 94 | return Err(std::io::Error::last_os_error().into()); 95 | } 96 | let io_counters = unsafe { io_counters.assume_init() }; 97 | Ok(IOStats { 98 | read_count: io_counters.ReadOperationCount, 99 | write_count: io_counters.WriteOperationCount, 100 | read_bytes: io_counters.ReadTransferCount, 101 | write_bytes: io_counters.WriteTransferCount, 102 | }) 103 | } 104 | 105 | #[cfg(target_os = "macos")] 106 | fn get_process_io_stats_impl() -> Result { 107 | use libc::{rusage_info_v2, RUSAGE_INFO_V2}; 108 | use std::{mem::MaybeUninit, os::raw::c_int}; 109 | 110 | let mut rusage_info_v2 = MaybeUninit::::uninit(); 111 | let ret_code = unsafe { 112 | libc::proc_pid_rusage( 113 | std::process::id() as c_int, 114 | RUSAGE_INFO_V2, 115 | rusage_info_v2.as_mut_ptr() as *mut _, 116 | ) 117 | }; 118 | if ret_code != 0 { 119 | return Err(std::io::Error::last_os_error().into()); 120 | } 121 | let rusage_info_v2 = unsafe { rusage_info_v2.assume_init() }; 122 | Ok(IOStats { 123 | read_bytes: rusage_info_v2.ri_diskio_bytesread, 124 | write_bytes: rusage_info_v2.ri_diskio_byteswritten, 125 | ..Default::default() 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provide the ability to retrieve information for profiling. 2 | //! 3 | //! 4 | 5 | #![cfg_attr(test, allow(clippy::all, clippy::unwrap_used))] 6 | #![cfg_attr(doc, feature(doc_cfg))] 7 | #![cfg_attr(test, feature(test))] 8 | 9 | #[cfg(test)] 10 | extern crate test; 11 | 12 | #[allow(warnings)] 13 | #[cfg(any(target_os = "macos", target_os = "ios"))] 14 | pub(crate) mod bindings { 15 | include!(concat!(env!("OUT_DIR"), "/monitor_rs_ios_macos_binding.rs")); 16 | } 17 | 18 | pub mod cpu; 19 | 20 | pub mod mem; 21 | 22 | pub mod io; 23 | 24 | pub mod fd; 25 | 26 | mod utils; 27 | -------------------------------------------------------------------------------- /src/mem/allocation_counter.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | alloc::{GlobalAlloc, Layout, System}, 3 | sync::atomic::{AtomicBool, AtomicIsize, Ordering}, 4 | }; 5 | 6 | pub const MIN_ALIGN: usize = 16; // module `sys_common` is private. https://doc.rust-lang.org/src/std/sys_common/alloc.rs.html#28 7 | 8 | static ALLOCATED: AtomicIsize = AtomicIsize::new(0); 9 | static ENABLE: AtomicBool = AtomicBool::new(false); 10 | 11 | /// An allocator tracks inuse allocated bytes. 12 | /// 13 | /// The counter is disable by default. Please enable it by `CountingAllocator::enable()` then call `CountingAllocator::get_allocated()` will return the bytes inused. 14 | pub struct CountingAllocator; 15 | 16 | impl CountingAllocator { 17 | /// Get the inuse bytes allocated by rust. 18 | pub fn get_allocated() -> isize { 19 | ALLOCATED.load(Ordering::SeqCst) 20 | } 21 | 22 | /// Check whether the counter is enable. 23 | pub fn is_enable() -> bool { 24 | ENABLE.load(Ordering::SeqCst) 25 | } 26 | 27 | /// Reset the counter. 28 | pub fn reset() { 29 | ALLOCATED.store(0, Ordering::SeqCst) 30 | } 31 | 32 | /// Enable the counter. 33 | pub fn enable() { 34 | ENABLE.store(true, Ordering::SeqCst) 35 | } 36 | 37 | /// Disable the counter. 38 | pub fn disable() { 39 | ENABLE.store(false, Ordering::SeqCst) 40 | } 41 | } 42 | 43 | unsafe impl GlobalAlloc for CountingAllocator { 44 | #[inline] 45 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 { 46 | let ret = System.alloc(layout); 47 | if !ret.is_null() && Self::is_enable() { 48 | ALLOCATED.fetch_add(layout.size() as isize, Ordering::SeqCst); 49 | } 50 | ret 51 | } 52 | 53 | #[inline] 54 | unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { 55 | System.dealloc(ptr, layout); 56 | if Self::is_enable() { 57 | ALLOCATED.fetch_sub(layout.size() as isize, Ordering::SeqCst); 58 | } 59 | } 60 | 61 | #[inline] 62 | unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { 63 | let ret: *mut u8 = System.realloc(ptr, layout, new_size); 64 | if !ret.is_null() 65 | && Self::is_enable() 66 | && layout.align() <= MIN_ALIGN 67 | && layout.align() <= new_size 68 | { 69 | ALLOCATED.fetch_add(new_size as isize - layout.size() as isize, Ordering::SeqCst); 70 | } 71 | ret 72 | } 73 | 74 | #[inline] 75 | unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { 76 | let ret = System.alloc_zeroed(layout); 77 | if !ret.is_null() && Self::is_enable() { 78 | ALLOCATED.fetch_add(layout.size() as isize, Ordering::SeqCst); 79 | } 80 | ret 81 | } 82 | } 83 | #[cfg(feature = "allocation_counter")] 84 | #[global_allocator] 85 | static _COUNTER: perf_monitor::mem::CountingAllocator = perf_monitor::mem::CountingAllocator; 86 | -------------------------------------------------------------------------------- /src/mem/apple/heap.rs: -------------------------------------------------------------------------------- 1 | //! A wrapper around libmalloc APIs. 2 | 3 | use crate::bindings::{ 4 | mach_task_self_, malloc_default_zone, malloc_statistics_t, malloc_zone_t, vm_address_t, 5 | }; 6 | use std::{io, str}; 7 | 8 | /// A Wrapper around `malloc_statistics_t`, originally defined at `libmalloc.h`. 9 | pub type MallocStatistics = malloc_statistics_t; 10 | 11 | /// A Wrapper around `malloc_zone_t`, originally defined at `libmalloc.h`. 12 | pub struct MallocZone(*mut malloc_zone_t); 13 | 14 | impl MallocZone { 15 | /// Get the name of this zone. 16 | pub fn name(&self) -> Result<&str, str::Utf8Error> { 17 | unsafe { std::ffi::CStr::from_ptr((*self.0).zone_name) }.to_str() 18 | } 19 | /// Get the statistics of this zone. 20 | pub fn statistics(&mut self) -> Option { 21 | unsafe { 22 | let mut stats = std::mem::MaybeUninit::::zeroed(); 23 | if let Some(f) = (*((*self.0).introspect)).statistics { 24 | f(self.0, stats.as_mut_ptr()); 25 | Some(stats.assume_init()) 26 | } else { 27 | None 28 | } 29 | } 30 | } 31 | } 32 | /// Get all malloc zones of current process. 33 | /// 34 | /// # Safety 35 | /// CAUTION: `MallocZone`s(*malloc_zone_t) returned by `malloc_get_all_zones` 36 | /// may be destoryed by other threads. 37 | pub unsafe fn malloc_get_all_zones() -> io::Result> { 38 | let mut count: u32 = 0; 39 | let mut zones: *mut vm_address_t = std::ptr::null_mut(); 40 | let ret = crate::bindings::malloc_get_all_zones(mach_task_self_, None, &mut zones, &mut count); 41 | if ret != 0 { 42 | Err(io::Error::from_raw_os_error(ret)) 43 | } else { 44 | let zones = 45 | std::slice::from_raw_parts_mut(zones as *mut *mut malloc_zone_t, count as usize) 46 | .iter() 47 | .map(|&p| MallocZone(p)) 48 | .collect::>(); 49 | Ok(zones) 50 | } 51 | } 52 | 53 | /// Get the default malloc zone of current process. 54 | pub fn malloc_get_default_zone() -> MallocZone { 55 | MallocZone(unsafe { malloc_default_zone() }) 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::*; 61 | 62 | #[test] 63 | fn test_malloc_get_all_zones() { 64 | let zones = unsafe { malloc_get_all_zones().unwrap() }; 65 | assert!(!zones.is_empty()); 66 | let zone_names = zones.iter().map(|z| z.name().unwrap()).collect::>(); 67 | assert!(zone_names.contains(&"DefaultMallocZone")); 68 | } 69 | 70 | #[test] 71 | fn test_malloc_get_default_zone() { 72 | let zone = malloc_get_default_zone(); 73 | assert_eq!(zone.name().unwrap(), "DefaultMallocZone"); 74 | } 75 | 76 | #[test] 77 | fn test_malloc_zone_statistics() { 78 | let zones = unsafe { malloc_get_all_zones() }.unwrap(); 79 | for mut zone in zones { 80 | let stat = zone.statistics().unwrap(); 81 | assert!(stat.blocks_in_use > 0); 82 | assert!(stat.size_in_use > 0); 83 | assert!(stat.max_size_in_use > 0); 84 | assert!(stat.size_allocated > 0); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/mem/apple/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod heap; 2 | pub mod vm; 3 | -------------------------------------------------------------------------------- /src/mem/apple/vm.rs: -------------------------------------------------------------------------------- 1 | //! A wrapper around `mach_vm_region` API. 2 | 3 | use mach::{ 4 | kern_return::KERN_SUCCESS, 5 | mach_types::vm_task_entry_t, 6 | message::mach_msg_type_number_t, 7 | port::mach_port_t, 8 | traps::mach_task_self, 9 | vm_page_size::vm_page_size, 10 | vm_region::{vm_region_extended_info_data_t, vm_region_info_t, VM_REGION_EXTENDED_INFO}, 11 | vm_types::{mach_vm_address_t, mach_vm_size_t}, 12 | }; 13 | use std::{marker::PhantomData, mem}; 14 | 15 | // `vm_region_info_t`'s `user_tag`, originally defined at osfmk/mach/vm_statistics.h 16 | #[derive(Debug)] 17 | pub enum VMRegionKind { 18 | Malloc, 19 | MallocSmall, 20 | MallocLarge, 21 | MallocHuge, 22 | Sbrk, 23 | Realloc, 24 | MallocTiny, 25 | MallocLargeReusable, 26 | MallocLargeReused, 27 | Stack, 28 | MallocNano, 29 | Dylib, 30 | Dyld, 31 | DyldMalloc, 32 | Tag(u32), 33 | } 34 | 35 | impl From for VMRegionKind { 36 | fn from(user_tag: u32) -> Self { 37 | match user_tag { 38 | 1 => VMRegionKind::Malloc, 39 | 2 => VMRegionKind::MallocSmall, 40 | 3 => VMRegionKind::MallocLarge, 41 | 4 => VMRegionKind::MallocHuge, 42 | 5 => VMRegionKind::Sbrk, 43 | 6 => VMRegionKind::Realloc, 44 | 7 => VMRegionKind::MallocTiny, 45 | 8 => VMRegionKind::MallocLargeReusable, 46 | 9 => VMRegionKind::MallocLargeReused, 47 | 11 => VMRegionKind::MallocNano, 48 | 30 => VMRegionKind::Stack, 49 | 33 => VMRegionKind::Dylib, 50 | 60 => VMRegionKind::Dyld, 51 | 61 => VMRegionKind::DyldMalloc, 52 | tag => VMRegionKind::Tag(tag), 53 | } 54 | } 55 | } 56 | // A wrapper around `vm_region_extended_info` with addr, size ... props. 57 | #[derive(Debug)] 58 | pub struct VMRegion { 59 | addr: mach_vm_address_t, 60 | size: mach_vm_size_t, 61 | info: mach::vm_region::vm_region_extended_info, 62 | } 63 | 64 | impl VMRegion { 65 | pub fn kind(&self) -> VMRegionKind { 66 | VMRegionKind::from(self.info.user_tag) 67 | } 68 | 69 | pub fn dirty_bytes(&self) -> usize { 70 | self.info.pages_dirtied as usize * unsafe { vm_page_size } 71 | } 72 | 73 | pub fn swapped_bytes(&self) -> usize { 74 | self.info.pages_swapped_out as usize * unsafe { vm_page_size } 75 | } 76 | 77 | pub fn resident_bytes(&self) -> usize { 78 | self.info.pages_dirtied as usize * unsafe { vm_page_size } 79 | } 80 | 81 | fn end_addr(&self) -> mach_vm_address_t { 82 | self.addr + self.size as mach_vm_address_t 83 | } 84 | } 85 | // An iter over VMRegions. 86 | pub struct VMRegionIter { 87 | task: vm_task_entry_t, 88 | addr: mach_vm_address_t, 89 | _mark: PhantomData<*const ()>, // make it !Sync & !Send 90 | } 91 | 92 | impl Default for VMRegionIter { 93 | fn default() -> Self { 94 | Self { 95 | task: unsafe { mach_task_self() } as vm_task_entry_t, 96 | addr: 1, 97 | _mark: PhantomData, 98 | } 99 | } 100 | } 101 | 102 | impl Iterator for VMRegionIter { 103 | type Item = VMRegion; 104 | 105 | fn next(&mut self) -> Option { 106 | let mut count = mem::size_of::() as mach_msg_type_number_t; 107 | let mut object_name: mach_port_t = 0; 108 | let mut size = unsafe { mem::zeroed::() }; 109 | let mut info = unsafe { mem::zeroed::() }; 110 | let result = unsafe { 111 | mach::vm::mach_vm_region( 112 | self.task, 113 | &mut self.addr, 114 | &mut size, 115 | VM_REGION_EXTENDED_INFO, 116 | &mut info as *mut vm_region_extended_info_data_t as vm_region_info_t, 117 | &mut count, 118 | &mut object_name, 119 | ) 120 | }; 121 | if result != KERN_SUCCESS { 122 | None 123 | } else { 124 | let region = VMRegion { 125 | addr: self.addr, 126 | size, 127 | info, 128 | }; 129 | self.addr = region.end_addr(); 130 | Some(region) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/mem/mod.rs: -------------------------------------------------------------------------------- 1 | //! This sub-mod provides some facilities about memory performance profiling. 2 | //! # Memory usage of current process 3 | //! There's a platform-related function called `get_process_memory_info` available on MacOS and Windows. 4 | //! # Memory usage of ALL Rust allocations 5 | //! We provide a `CountingAllocator` that wraps the system allocator but tracks the bytes used by rust allocations. 6 | //! This crate DOES NOT replace the global allocator by default. You need to make it as a `global_allocator` or enable the `allocation_counter` feature. 7 | //! ```ignore 8 | //! #[global_allocator] 9 | //! static _COUNTER: perf_monitor::mem::CountingAllocator = perf_monitor:mem::CountingAllocator; 10 | //! ``` 11 | 12 | mod allocation_counter; 13 | 14 | pub use allocation_counter::CountingAllocator; 15 | 16 | mod process_memory_info; 17 | pub use process_memory_info::{get_process_memory_info, ProcessMemoryInfo}; 18 | 19 | #[cfg(target_os = "macos")] 20 | #[cfg_attr(doc, doc(cfg(macos)))] 21 | pub mod apple; 22 | -------------------------------------------------------------------------------- /src/mem/process_memory_info.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error, Result}; 2 | 3 | /// Process Memory Info returned by `get_process_memory_info` 4 | #[derive(Clone, Default)] 5 | pub struct ProcessMemoryInfo { 6 | /// this is the non-swapped physical memory a process has used. 7 | /// On UNIX it matches `top`'s RES column. 8 | /// 9 | /// On Windows this is an alias for wset field and it matches "Mem Usage" 10 | /// column of taskmgr.exe. 11 | pub resident_set_size: u64, 12 | #[cfg(not(any(target_os = "android", target_os = "linux")))] 13 | #[cfg_attr(doc, doc(cfg(not(linux))))] 14 | pub resident_set_size_peak: u64, 15 | 16 | /// this is the total amount of virtual memory used by the process. 17 | /// On UNIX it matches `top`'s VIRT column. 18 | /// 19 | /// On Windows this is an alias for pagefile field and it matches "Mem 20 | /// Usage" "VM Size" column of taskmgr.exe. 21 | pub virtual_memory_size: u64, 22 | 23 | /// This is the sum of: 24 | /// 25 | /// + (internal - alternate_accounting) 26 | /// 27 | /// + (internal_compressed - alternate_accounting_compressed) 28 | /// 29 | /// + iokit_mapped 30 | /// 31 | /// + purgeable_nonvolatile 32 | /// 33 | /// + purgeable_nonvolatile_compressed 34 | /// 35 | /// + page_table 36 | /// 37 | /// details: 38 | #[cfg(any(target_os = "macos", target_os = "ios"))] 39 | #[cfg_attr(doc, doc(macos))] 40 | pub phys_footprint: u64, 41 | 42 | #[cfg(any(target_os = "macos", target_os = "ios"))] 43 | #[cfg_attr(doc, doc(macos))] 44 | pub compressed: u64, 45 | } 46 | 47 | #[cfg(target_os = "windows")] 48 | fn get_process_memory_info_impl() -> Result { 49 | use std::mem::MaybeUninit; 50 | use windows_sys::Win32::System::ProcessStatus::GetProcessMemoryInfo; 51 | use windows_sys::Win32::System::ProcessStatus::PROCESS_MEMORY_COUNTERS; 52 | use windows_sys::Win32::System::Threading::GetCurrentProcess; 53 | let mut process_memory_counters = MaybeUninit::::uninit(); 54 | let ret = unsafe { 55 | // If the function succeeds, the return value is nonzero. 56 | // If the function fails, the return value is zero. 57 | // https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getprocessmemoryinfo 58 | GetProcessMemoryInfo( 59 | GetCurrentProcess(), 60 | process_memory_counters.as_mut_ptr(), 61 | std::mem::size_of::() as u32, 62 | ) 63 | }; 64 | if ret == 0 { 65 | return Err(Error::last_os_error()); 66 | } 67 | let process_memory_counters = unsafe { process_memory_counters.assume_init() }; 68 | Ok(ProcessMemoryInfo { 69 | resident_set_size: process_memory_counters.WorkingSetSize as u64, 70 | resident_set_size_peak: process_memory_counters.PeakWorkingSetSize as u64, 71 | virtual_memory_size: process_memory_counters.PagefileUsage as u64, 72 | }) 73 | } 74 | 75 | #[cfg(any(target_os = "linux", target_os = "android"))] 76 | fn get_process_memory_info_impl() -> Result { 77 | // https://www.kernel.org/doc/Documentation/filesystems/proc.txt 78 | let statm = std::fs::read_to_string("/proc/self/statm")?; 79 | let mut parts = statm.split(' '); 80 | let Some(virtual_memory_size) = parts.next().and_then(|s| s.parse().ok()) else { 81 | return Err(Error::new(std::io::ErrorKind::Other, "Invalid VmSize in /proc/self/statm")); 82 | }; 83 | let Some(resident_set_size) = parts.next().and_then(|s| s.parse().ok()) else { 84 | return Err(Error::new(std::io::ErrorKind::Other, "Invalid VmRSS in /proc/self/statm")); 85 | }; 86 | Ok(ProcessMemoryInfo { 87 | virtual_memory_size, 88 | resident_set_size, 89 | }) 90 | } 91 | 92 | #[cfg(any(target_os = "macos", target_os = "ios"))] 93 | fn get_process_memory_info_impl() -> Result { 94 | use crate::bindings::task_vm_info; 95 | use mach::{ 96 | kern_return::KERN_SUCCESS, message::mach_msg_type_number_t, task::task_info, 97 | task_info::TASK_VM_INFO, traps::mach_task_self, vm_types::natural_t, 98 | }; 99 | use std::mem::MaybeUninit; 100 | 101 | let mut task_vm_info = MaybeUninit::::uninit(); 102 | 103 | // https://github.com/apple/darwin-xnu/blob/master/osfmk/mach/task_info.h line 396 104 | // #define TASK_VM_INFO_COUNT ((mach_msg_type_number_t) \ 105 | // (sizeof (task_vm_info_data_t) / sizeof (natural_t))) 106 | let mut task_info_cnt: mach_msg_type_number_t = (std::mem::size_of::() 107 | / std::mem::size_of::()) 108 | as mach_msg_type_number_t; 109 | 110 | let kern_ret = unsafe { 111 | task_info( 112 | mach_task_self(), 113 | TASK_VM_INFO, 114 | task_vm_info.as_mut_ptr() as *mut _, 115 | &mut task_info_cnt, 116 | ) 117 | }; 118 | if kern_ret != KERN_SUCCESS { 119 | // see https://docs.rs/mach/0.2.3/mach/kern_return/index.html for more details 120 | return Err(Error::new( 121 | std::io::ErrorKind::Other, 122 | format!("DARWIN_KERN_RET_CODE:{}", kern_ret), 123 | )); 124 | } 125 | let task_vm_info = unsafe { task_vm_info.assume_init() }; 126 | Ok(ProcessMemoryInfo { 127 | resident_set_size: task_vm_info.resident_size, 128 | resident_set_size_peak: task_vm_info.resident_size_peak, 129 | virtual_memory_size: task_vm_info.virtual_size, 130 | phys_footprint: task_vm_info.phys_footprint, 131 | compressed: task_vm_info.compressed, 132 | }) 133 | } 134 | 135 | pub fn get_process_memory_info() -> Result { 136 | get_process_memory_info_impl() 137 | } 138 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ptr_upgrade; 2 | #[cfg(windows)] 3 | pub mod windows_handle; 4 | -------------------------------------------------------------------------------- /src/utils/ptr_upgrade.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroIsize; 2 | 3 | /// Triage a return value of windows handle to `Some(handle)` or `None` 4 | pub trait HandleUpgrade: Sized { 5 | fn upgrade(self) -> Option; 6 | } 7 | 8 | impl HandleUpgrade for isize { 9 | #[inline] 10 | fn upgrade(self) -> Option { 11 | NonZeroIsize::new(self) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/windows_handle.rs: -------------------------------------------------------------------------------- 1 | use std::num::NonZeroIsize; 2 | use windows_sys::Win32::Foundation::CloseHandle; 3 | 4 | /// Windows handle wrapper 5 | pub struct Handle(NonZeroIsize); 6 | 7 | impl Handle { 8 | /// Invalid handle leads to UB 9 | pub unsafe fn new(handle: NonZeroIsize) -> Self { 10 | Handle(handle) 11 | } 12 | 13 | pub fn as_handle(&self) -> isize { 14 | self.0.get() 15 | } 16 | } 17 | 18 | impl Drop for Handle { 19 | fn drop(&mut self) { 20 | unsafe { CloseHandle(self.0.get()) }; 21 | } 22 | } 23 | --------------------------------------------------------------------------------