diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 64e8000..185113a 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -5,137 +5,15 @@ on: - workflow_dispatch jobs: - test-build: - name: Build & Test - strategy: - matrix: - os: - - ubuntu-20.04 - - macos-11 - - windows-2019 - runs-on: ${{ matrix.os }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Build - run: cargo build --all-features - - name: Test - uses: bp3d-actions/cargo@main - with: - check-name: cargo test (${{ matrix.os }}) - command: test - args: --all-features --no-fail-fast - token: ${{ secrets.GITHUB_TOKEN }} - - clippy: - name: Check | Clippy - if: ${{ always() }} - needs: test-build - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - name: Run check - uses: bp3d-actions/clippy-check@main - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features - - audit: - name: Check | Audit - if: ${{ always() }} - needs: test-build - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Install Audit Tool - run: cargo install cargo-audit - - name: Run check - uses: bp3d-actions/audit-check@main - with: - token: ${{ secrets.GITHUB_TOKEN }} - - fmt: - name: Format Code - if: ${{ always() && github.ref != 'refs/heads/master' }} - needs: - - clippy - - audit - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt - - name: Run code formatter - uses: bp3d-actions/rustfmt-check@main - with: - token: ${{ secrets.GITHUB_TOKEN }} - - version: - name: Get Version - needs: test-build - runs-on: ubuntu-latest - outputs: - name: ${{ steps.version.outputs.name }} - version: ${{ steps.version.outputs.version }} - isnew: ${{ steps.version.outputs.isnew }} - ispre: ${{ steps.version.outputs.ispre }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Get Version - id: version - uses: bp3d-actions/cargo-version@main - with: - mode: get - token: ${{ secrets.GITHUB_TOKEN }} - - create-pre-release: - name: Create Pre Release - needs: version - if: github.ref == 'refs/heads/develop' && needs.version.outputs.isnew == 'true' && needs.version.outputs.ispre == 'true' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Setup cargo - run: cargo login ${{ secrets.RELEASE_TOKEN }} - - name: Publish - run: cargo publish - - name: Create - uses: ncipollo/release-action@main - with: - tag: ${{ needs.version.outputs.version }} - commit: ${{ github.ref }} - prerelease: true - name: ${{ needs.version.outputs.name }} release ${{ needs.version.outputs.version }} - body: "[Link to crates.io](https://crates.io/crates/${{ needs.version.outputs.name }})" - - create-release-pr: - name: Create Release Pull Request - needs: version - if: needs.version.outputs.isnew == 'true' && needs.version.outputs.ispre == 'false' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Create Pull Request - uses: repo-sync/pull-request@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - destination_branch: master - pr_title: Release ${{ needs.version.outputs.version }} + Test: + uses: BlockProject3D/workflows/.github/workflows/Build_Test.yml@main + + Analyze: + uses: BlockProject3D/workflows/.github/workflows/Analyze.yml@main + needs: Test + + Release: + uses: BlockProject3D/workflows/.github/workflows/Release.yml@main + needs: Test + secrets: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf2e432..b4df58a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,43 +6,7 @@ on: - master jobs: - version: - name: Get Version - runs-on: ubuntu-latest - outputs: - name: ${{ steps.version.outputs.name }} - version: ${{ steps.version.outputs.version }} - isnew: ${{ steps.version.outputs.isnew }} - ispre: ${{ steps.version.outputs.ispre }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Get Version - id: version - uses: bp3d-actions/cargo-version@main - with: - mode: get - token: ${{ secrets.GITHUB_TOKEN }} - - create-release: - name: Release - needs: version - if: needs.version.outputs.isnew == 'true' && needs.version.outputs.ispre == 'false' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Setup cargo - run: cargo login ${{ secrets.RELEASE_TOKEN }} - - name: Publish - run: cargo publish - - name: Create - uses: ncipollo/release-action@main - with: - tag: ${{ needs.version.outputs.version }} - commit: ${{ github.ref }} - prerelease: false - name: ${{ needs.version.outputs.name }} release ${{ needs.version.outputs.version }} - body: "[Link to crates.io](https://crates.io/crates/${{ needs.version.outputs.name }})" + Publish: + uses: BlockProject3D/workflows/.github/workflows/Publish.yml@main + secrets: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index d1a5d95..4126c1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,13 @@ [package] -name = "bp3d-logger" -version = "1.1.0" +name = "bp3d-debug" +version = "1.0.0" authors = ["Yuri Edward "] edition = "2021" -description = "A flexible Log implementation intended to be used with BP3D software." +description = "Tracing subscriber implementations for use with BP3D software. Supports traditional logging through bp3d-logger and supports remote profiling through TCP." license = "BSD-3-Clause" -repository = "https://gitlab.com/bp3d/logger" -readme = "./README.MD" -keywords = [] -categories = [] +repository = "https://github.com/BlockProject3D/debug.core" +readme = "README.MD" +keywords = ["bp3d", "tracing"] +categories = ["development-tools", "development-tools::debugging", "development-tools::profiling"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -log = "0.4.14" -bp3d-fs = "1.0.0" -crossbeam-channel = "0.5.2" -once_cell = "1.10.0" -time = { version = "0.3.7", features = ["formatting", "macros"] } -time-tz = { version = "0.3.1", features = ["system"] } -termcolor = "1.1.3" -atty = "0.2.14" - -[features] diff --git a/LICENSE.txt b/LICENSE.txt index f180790..83dbf64 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2021, BlockProject 3D +Copyright (c) 2024, BlockProject 3D All rights reserved. diff --git a/README.MD b/README.MD index 8695fc9..8a47d7f 100644 --- a/README.MD +++ b/README.MD @@ -1,23 +1,11 @@ ![logo](https://assets.gitlab-static.net/uploads/-/system/group/avatar/10553166/logo_white.ico?width=64) -# BP3D logger +# Debug core -A flexible [Log](https://docs.rs/log/latest/log/trait.Log.html) implementation intended to be used with BP3D software. +The debug core contains Rust APIs used to debug BP3D software. This contains a set of tools optimized for use by BP3D based projects with soft real-time constraints. -## Main features -- Support for file logging. -- Support for stdout/stderr logging. -- Support for in memory logger. -- Easily switch on/off logging backends. -- Logging backends should not panic/abort/crash the software in any way. - -### stdout/stderr -- Error messages are written to stderr instead of stdout (configurable). -- If write fails, message is ignored; no panic produced. - -### file logging -- If write fails, an error is sent to other logging backends, if other backends fails, message is ignored; no panic produced. - -### in memory -- Log is limited to a fixed number of messages (configurable). +## Features +- A logger system with trace disabled in release builds for improved performance. +- A simple profiler system which can efficiently measure the time spent in Rust code scope. +- A trace system designed to trace asynchronous and long-running operations. diff --git a/src/backend.rs b/src/backend.rs deleted file mode 100644 index c7a22de..0000000 --- a/src/backend.rs +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) 2021, BlockProject 3D -// -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without modification, -// are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// * Neither the name of BlockProject 3D nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -use log::Level; -use std::collections::HashMap; -use std::fmt::Display; -use std::fmt::Formatter; -use std::fs::{File, OpenOptions}; -use std::io::{BufWriter, Write}; -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; -use atty::Stream; -use termcolor::{ColorChoice, ColorSpec, StandardStream}; -use crate::Colors; -use crate::easy_termcolor::{color, EasyTermColor}; - -pub trait Backend { - type Error: Display; - fn write(&mut self, target: &str, msg: &str, level: Level) -> Result<(), Self::Error>; - fn flush(&mut self) -> Result<(), Self::Error>; -} - -pub struct DummyError(); - -impl Display for DummyError { - fn fmt(&self, _: &mut Formatter<'_>) -> std::fmt::Result { - todo!() // Panic (DummyError is by definition the error that never occurs)! - } -} - -pub static ENABLE_STDOUT: AtomicBool = AtomicBool::new(true); - -pub struct StdBackend { - smart_stderr: bool, - colors: Colors -} - -fn write_msg(stream: StandardStream, target: &str, msg: &str, level: Level) { - let t = ColorSpec::new().set_bold(true).clone(); - EasyTermColor(stream).write('<').color(t).write(target).reset().write("> ") - .write('[').color(color(level)).write(level).reset().write(']') - .write(format!(" {}", msg)).lf(); -} - -impl StdBackend { - pub fn new(smart_stderr: bool, colors: Colors) -> StdBackend { - StdBackend { smart_stderr, colors } - } - - fn get_stream(&self, level: Level) -> Stream { - match self.smart_stderr { - false => Stream::Stdout, - true => match level { - Level::Error => Stream::Stderr, - _ => Stream::Stdout - } - } - } -} - -impl Backend for StdBackend { - type Error = DummyError; - - fn write(&mut self, target: &str, msg: &str, level: Level) -> Result<(), Self::Error> { - if !ENABLE_STDOUT.load(Ordering::Acquire) { - // Skip logging if temporarily disabled. - return Ok(()); - } - let stream = self.get_stream(level); - let use_termcolor = match self.colors { - Colors::Disabled => false, - Colors::Enabled => true, - Colors::Auto => atty::is(stream) - }; - match use_termcolor { - true => { - let val = match stream { - Stream::Stderr => StandardStream::stderr(ColorChoice::Always), - _ => StandardStream::stdout(ColorChoice::Always) - }; - write_msg(val, target, msg, level); - }, - false => { - match stream { - Stream::Stderr => eprintln!("<{}> [{}] {}", target, level, msg), - _ => println!("<{}> [{}] {}", target, level, msg) - }; - } - }; - Ok(()) - } - - fn flush(&mut self) -> Result<(), Self::Error> { - Ok(()) - } -} - -pub struct FileBackend { - targets: HashMap>, - path: PathBuf, -} - -impl FileBackend { - pub fn new(path: PathBuf) -> FileBackend { - FileBackend { - targets: HashMap::new(), - path, - } - } - - fn get_create_open_file( - &mut self, - target: &str, - ) -> Result<&mut BufWriter, std::io::Error> { - if self.targets.get(target).is_none() { - let f = OpenOptions::new() - .append(true) - .create(true) - .open(self.path.join(format!("{}.log", target)))?; - self.targets.insert(target.into(), BufWriter::new(f)); - } - unsafe { - // This cannot never fail because None is captured and initialized by the if block. - Ok(self.targets.get_mut(target).unwrap_unchecked()) - } - } -} - -impl Backend for FileBackend { - type Error = std::io::Error; - - fn write(&mut self, target: &str, msg: &str, level: Level) -> Result<(), Self::Error> { - writeln!(self.get_create_open_file(target)?, "[{}] {}", level, msg) - } - - fn flush(&mut self) -> Result<(), Self::Error> { - for v in self.targets.values_mut() { - v.flush()?; - } - Ok(()) - } -} diff --git a/src/engine/default.rs b/src/engine/default.rs new file mode 100644 index 0000000..9511325 --- /dev/null +++ b/src/engine/default.rs @@ -0,0 +1,92 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::engine::ENGINE_INIT_FLAG; +use crate::field::Field; +use crate::trace::span::{Callsite, Id}; +use std::fmt::Arguments; +use std::num::NonZeroU32; +use std::sync::atomic::Ordering; + +pub struct DefaultDebugger {} + +impl crate::profiler::Profiler for DefaultDebugger { + fn section_register(&self, _: &'static crate::profiler::section::Section) -> NonZeroU32 { + ENGINE_INIT_FLAG.store(true, Ordering::Relaxed); + unsafe { NonZeroU32::new_unchecked(1) } + } + + fn section_record(&self, _: NonZeroU32, _: u64, _: u64, _: &[Field]) { + ENGINE_INIT_FLAG.store(true, Ordering::Relaxed); + } +} + +impl crate::trace::Tracer for DefaultDebugger { + fn register_callsite(&self, _: &'static Callsite) -> NonZeroU32 { + ENGINE_INIT_FLAG.store(true, Ordering::Relaxed); + unsafe { NonZeroU32::new_unchecked(1) } + } + + fn span_create(&self, _: NonZeroU32, _: &[Field]) -> NonZeroU32 { + ENGINE_INIT_FLAG.store(true, Ordering::Relaxed); + unsafe { NonZeroU32::new_unchecked(1) } + } + + fn span_enter(&self, _: Id) { + ENGINE_INIT_FLAG.store(true, Ordering::Relaxed); + } + + fn span_record(&self, _: Id, _: &[Field]) { + ENGINE_INIT_FLAG.store(true, Ordering::Relaxed); + } + + fn span_exit(&self, _: Id) { + ENGINE_INIT_FLAG.store(true, Ordering::Relaxed); + } + + fn span_destroy(&self, _: Id) { + ENGINE_INIT_FLAG.store(true, Ordering::Relaxed); + } +} + +impl crate::logger::Logger for DefaultDebugger { + fn log(&self, callsite: &'static crate::logger::Callsite, args: Arguments, fields: &[Field]) { + let mut s = String::new(); + for field in fields { + s += &format!(", {}={}", field.name(), field.value()); + } + println!( + "[{}] {}: {}{}", + callsite.level(), + callsite.location().module_path(), + args, + s + ); + ENGINE_INIT_FLAG.store(true, Ordering::Relaxed); + } +} diff --git a/src/engine/mod.rs b/src/engine/mod.rs new file mode 100644 index 0000000..c629ff2 --- /dev/null +++ b/src/engine/mod.rs @@ -0,0 +1,82 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::sync::atomic::{AtomicBool, Ordering}; + +mod default; + +pub trait Engine: + crate::logger::Logger + crate::profiler::Profiler + crate::trace::Tracer + Sync +{ +} +impl Engine + for T +{ +} + +static ENGINE_INIT_FLAG: AtomicBool = AtomicBool::new(false); + +static mut ENGINE: &dyn Engine = &default::DefaultDebugger {}; + +pub fn get() -> &'static dyn Engine { + unsafe { ENGINE } +} + +pub fn set(engine: &'static dyn Engine) -> bool { + let flag = ENGINE_INIT_FLAG.load(Ordering::Relaxed); + if flag { + return false; + } + unsafe { ENGINE = engine }; + ENGINE_INIT_FLAG.store(true, Ordering::Relaxed); + true +} + +#[cfg(test)] +mod tests { + use crate::trace::span::Id; + use std::num::NonZeroU32; + + #[test] + fn basic() { + crate::engine::set(&crate::engine::default::DefaultDebugger {}); + assert!(!crate::engine::set( + &crate::engine::default::DefaultDebugger {} + )); + } + + #[test] + fn after_use() { + crate::engine::get().span_exit(Id::new(unsafe { NonZeroU32::new_unchecked(1) }, unsafe { + NonZeroU32::new_unchecked(1) + })); + assert!(!crate::engine::set( + &crate::engine::default::DefaultDebugger {} + )); + } +} diff --git a/src/field.rs b/src/field.rs new file mode 100644 index 0000000..b99fdb8 --- /dev/null +++ b/src/field.rs @@ -0,0 +1,157 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::fmt::{Debug, Display, Formatter}; + +#[derive(Debug)] +pub enum FieldValue<'a> { + Int(i64), + UInt(u64), + Float(f32), + Double(f64), + String(&'a str), + Debug(&'a dyn Debug), + Boolean(bool), +} + +impl Display for FieldValue<'_> { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + FieldValue::Int(v) => write!(f, "{}", v), + FieldValue::UInt(v) => write!(f, "{}", v), + FieldValue::Float(v) => write!(f, "{}", v), + FieldValue::Double(v) => write!(f, "{}", v), + FieldValue::String(v) => f.write_str(v), + FieldValue::Debug(v) => write!(f, "{:?}", v), + FieldValue::Boolean(v) => write!(f, "{:?}", v), + } + } +} + +pub struct Field<'a> { + name: &'a str, + value: FieldValue<'a>, +} + +impl<'a> Field<'a> { + pub fn new(name: &'a str, value: impl Into>) -> Self { + Self { + name, + value: value.into(), + } + } + + pub fn new_debug(name: &'a str, value: &'a dyn Debug) -> Self { + Self { + name, + value: FieldValue::Debug(value), + } + } + + pub fn name(&self) -> &str { + self.name + } + + pub fn value(&self) -> &FieldValue<'a> { + &self.value + } +} + +macro_rules! impl_into_field_value { + // Would've preferred expr, but turns out expr is useless in macros, so let's not use it. + ($($t: ty => $func: ident),*) => { + $( + impl<'a> From<$t> for FieldValue<'a> { + fn from(value: $t) -> Self { + FieldValue::$func(value as _) + } + } + )* + }; +} + +impl_into_field_value! { + u8 => UInt, + u16 => UInt, + u32 => UInt, + u64 => UInt, + usize => UInt, + i8 => Int, + i16 => Int, + i32 => Int, + i64 => Int, + isize => Int, + f32 => Float, + f64 => Double, + bool => Boolean +} + +impl<'a> From<&'a str> for FieldValue<'a> { + fn from(value: &'a str) -> Self { + Self::String(value) + } +} + +pub struct FieldSet<'a, const N: usize>([Field<'a>; N]); + +impl<'a, const N: usize> FieldSet<'a, N> { + pub fn new(fields: [Field<'a>; N]) -> Self { + Self(fields) + } +} + +impl<'a, const N: usize> AsRef<[Field<'a>]> for FieldSet<'a, N> { + fn as_ref(&self) -> &[Field<'a>] { + &self.0 + } +} + +#[macro_export] +macro_rules! field { + ($name: ident) => { + $crate::field::Field::new(stringify!($name), $name) + }; + (?$name: ident) => { + $crate::field::Field::new_debug(stringify!($name), &$name) + }; + ($name: ident = $value: expr) => { + $crate::field::Field::new(stringify!($name), $value) + }; + ($name: ident = ?$value: expr) => { + $crate::field::Field::new_debug(stringify!($name), &$value) + }; +} + +#[macro_export] +macro_rules! fields { + ($({$($field: tt)*})*) => { + [$( + $crate::field!($($field)*), + )*] + }; +} diff --git a/src/internal.rs b/src/internal.rs deleted file mode 100644 index 9f8c091..0000000 --- a/src/internal.rs +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) 2021, BlockProject 3D -// -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without modification, -// are permitted provided that the following conditions are met: -// -// * Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// * Neither the name of BlockProject 3D nor the names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -use crate::backend::Backend; -use crate::{LogMsg, Logger}; -use crossbeam_channel::{bounded, Receiver, Sender}; -use log::{Level, Log, Metadata, Record}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Mutex; -use time::macros::format_description; -use time::OffsetDateTime; -use time_tz::OffsetDateTimeExt; - -const BUF_SIZE: usize = 128; // The maximum count of log messages in the channel. - -enum Command { - Flush, - Log(LogMsg), - Terminate, -} - -fn log( - backend: Option<&mut T>, - target: &str, - msg: &str, - level: Level, -) -> Result<(), T::Error> { - if let Some(back) = backend { - back.write(target, msg, level) - } else { - Ok(()) - } -} - -fn exec_commad(cmd: Command, logger: &mut Logger) -> bool { - match cmd { - Command::Terminate => true, - Command::Flush => { - if let Some(file) = &mut logger.file { - if let Err(e) = file.flush() { - let _ = log( - logger.std.as_mut(), - "bp3d-logger", - &format!("Could not flush file backend: {}", e), - Level::Error, - ); - } - } - false - } - Command::Log(LogMsg { target, msg, level }) => { - if let Err(e) = log(logger.file.as_mut(), &target, &msg, level) { - let _ = log( - logger.std.as_mut(), - "bp3d-logger", - &format!("Could not write to file backend: {}", e), - Level::Error, - ); - } - let _ = log(logger.std.as_mut(), &target, &msg, level); - false - } - } -} - -pub struct LoggerImpl { - thread: Mutex>>, - send_ch: Sender, - recv_ch: Receiver, - log_buffer_send_ch: Sender, - log_buffer_recv_ch: Receiver, - enable_log_buffer: AtomicBool, - enabled: AtomicBool, -} - -impl LoggerImpl { - pub fn new() -> LoggerImpl { - let (send_ch, recv_ch) = bounded(BUF_SIZE); - let (log_buffer_send_ch, log_buffer_recv_ch) = bounded(BUF_SIZE); - LoggerImpl { - thread: Mutex::new(None), - send_ch, - recv_ch, - log_buffer_send_ch, - log_buffer_recv_ch, - enable_log_buffer: AtomicBool::new(false), - enabled: AtomicBool::new(false), - } - } - - pub fn enable(&self, flag: bool) { - self.enabled.store(flag, Ordering::Release); - } - - pub fn enable_log_buffer(&self, flag: bool) { - self.enable_log_buffer.store(flag, Ordering::Release); - } - - pub fn clear_log_buffer(&self) { - while self.log_buffer_recv_ch.try_recv().is_ok() {} //Clear the entire log buffer. - } - - pub fn get_log_buffer(&self) -> Receiver { - self.log_buffer_recv_ch.clone() - } - - pub fn terminate(&self) { - // This should never panic as there's no way another call would have panicked! - let mut thread = self.thread.lock().unwrap(); - if let Some(handle) = thread.take() { - // This cannot panic as send_ch is owned by LoggerImpl which is intended - // to be statically allocated. - unsafe { - self.send_ch.send(Command::Flush).unwrap_unchecked(); - self.send_ch.send(Command::Terminate).unwrap_unchecked(); - } - // Join the logging thread; this will lock until the thread is completely terminated. - handle.join().unwrap(); - } - } - - pub fn start_new_thread(&self, logger: Logger) { - let mut flag = false; - { - // This should never panic as there's no way another call would have panicked! - let mut thread = self.thread.lock().unwrap(); - if let Some(handle) = thread.take() { - // This cannot panic as send_ch is owned by LoggerImpl which is intended - // to be statically allocated. - unsafe { - self.send_ch.send(Command::Terminate).unwrap_unchecked(); - } - if handle.join().is_err() { - flag = true; - } - } - let recv_ch = self.recv_ch.clone(); - *thread = Some(std::thread::spawn(move || { - let mut logger = logger; - while let Ok(v) = recv_ch.recv() { - let flag = exec_commad(v, &mut logger); - if flag { - // The thread has requested to exit itself; drop out of the main loop. - break; - } - } - })); - } - if flag { - // Somehow the previous thread has panicked; log that panic... - unsafe { - // This cannot panic as send_ch is owned by LoggerImpl which is intended - // to be statically allocated. - self.send_ch - .send(Command::Log(LogMsg { - level: Level::Error, - msg: "The logging thread has panicked!".into(), - target: "bp3d-logger".into(), - })) - .unwrap_unchecked(); - } - } - } - - pub fn low_level_log(&self, msg: LogMsg) { - if self.enable_log_buffer.load(Ordering::Acquire) { - unsafe { - // This cannot panic as both send_ch and log_buffer_send_ch are owned by LoggerImpl - // which is intended to be statically allocated. - self.send_ch - .send(Command::Log(msg.clone())) - .unwrap_unchecked(); - self.log_buffer_send_ch.send(msg).unwrap_unchecked(); - } - } else { - unsafe { - // This cannot panic as send_ch is owned by LoggerImpl which is intended - // to be statically allocated. - self.send_ch.send(Command::Log(msg)).unwrap_unchecked(); - } - } - } - - pub fn is_enabled(&self) -> bool { - self.enabled.load(Ordering::Acquire) - } -} - -fn extract_target_module<'a>(record: &'a Record) -> (&'a str, Option<&'a str>) { - let base_string = record.module_path().unwrap_or_else(|| record.target()); - let target = base_string - .find("::") - .map(|v| &base_string[..v]) - .unwrap_or(base_string); - let module = base_string.find("::").map(|v| &base_string[(v + 2)..]); - (target, module) -} - -impl Log for LoggerImpl { - fn enabled(&self, _: &Metadata) -> bool { - self.is_enabled() - } - - fn log(&self, record: &Record) { - // Apparently the log crate is defective: the enabled function is ignored... - if !self.enabled(record.metadata()) { - return; - } - let (target, module) = extract_target_module(record); - //In the future attempt to not update all the time https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=17c218f318826f55ab64535bfcd28ec6 - let system_tz = - time_tz::system::get_timezone().unwrap_or(time_tz::timezones::db::us::CENTRAL); - let format = format_description!("[weekday repr:short] [month repr:short] [day] [hour repr:12]:[minute]:[second] [period case:upper]"); - // is very unlikely to occur (only possibility is a weird io error). - let formatted = OffsetDateTime::now_utc() - .to_timezone(system_tz) - .format(format) - .unwrap_or_else(|_| "".into()); - let msg = LogMsg { - msg: format!( - "({}) {}: {}", - formatted, - module.unwrap_or("main"), - record.args() - ), - target: target.into(), - level: record.level(), - }; - self.low_level_log(msg); - } - - fn flush(&self) { - if !self.is_enabled() { - return; - } - unsafe { - // This cannot panic as send_ch is owned by LoggerImpl which is intended - // to be statically allocated. - self.send_ch.send(Command::Flush).unwrap_unchecked(); - while !self.send_ch.is_empty() {} - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 2fe19d2..37ccd5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2021, BlockProject 3D +// Copyright (c) 2024, BlockProject 3D // // All rights reserved. // @@ -26,296 +26,9 @@ // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// The reason why this is needed is because the 3 examples of usage of the Logger struct requires -// some context to not make it confusing. -#![allow(clippy::needless_doctest_main)] - -mod backend; -mod internal; -mod easy_termcolor; - -use bp3d_fs::dirs::App; -use crossbeam_channel::Receiver; -use log::{Level, Log}; -use once_cell::sync::Lazy; -use std::path::PathBuf; -use std::sync::atomic::Ordering; -use crate::backend::ENABLE_STDOUT; - -/// Represents a log message in the [LogBuffer](crate::LogBuffer). -#[derive(Clone)] -pub struct LogMsg { - /// The message string. - pub msg: String, - - /// The crate name that issued this log. - pub target: String, - - /// The log level. - pub level: Level, -} - -/// The log buffer type. -pub type LogBuffer = Receiver; - -/// Trait to allow getting a log directory from either a bp3d_fs::dirs::App or a String. -pub trait GetLogs { - /// Gets the log directory as a PathBuf. - /// - /// Returns None if no directory could be computed. - fn get_logs(self) -> Option; -} - -impl<'a> GetLogs for &'a String { - fn get_logs(self) -> Option { - self.as_str().get_logs() - } -} - -impl<'a, 'b> GetLogs for &'a App<'b> { - fn get_logs(self) -> Option { - self.get_logs().map(|v| v.into()).ok() - } -} - -impl<'a> GetLogs for &'a str { - fn get_logs(self) -> Option { - let app = App::new(self); - app.get_logs().map(|v| v.into()).ok() - } -} - -/// Enum of the different color settings when printing to stdout/stderr. -#[derive(Debug, Copy, Clone)] -pub enum Colors { - /// Color printing is always enabled. - Enabled, - - /// Color printing is always disabled. - Disabled, - - /// Color printing is automatic (if current terminal is a tty, print with colors, otherwise - /// print without colors). - Auto -} - -impl Default for Colors { - fn default() -> Self { - Self::Disabled - } -} - -/// The base logger builder/initializer. -/// -/// # Examples -/// -/// The following example shows basic initialization of this logger. -/// ``` -/// use bp3d_logger::Logger; -/// use log::info; -/// use log::LevelFilter; -/// -/// fn main() { -/// let _guard = Logger::new().add_stdout().add_file("my-app").start(); -/// log::set_max_level(LevelFilter::Info); -/// //... -/// info!("Example message"); -/// } -/// ``` -/// -/// The following example shows initialization of this logger with a return value. -/// ``` -/// use bp3d_logger::Logger; -/// use bp3d_logger::with_logger; -/// use log::info; -/// use log::LevelFilter; -/// -/// fn main() { -/// let code = with_logger(Logger::new().add_stdout().add_file("my-app"), || { -/// log::set_max_level(LevelFilter::Info); -/// //... -/// info!("Example message"); -/// 0 -/// }); -/// std::process::exit(code); -/// } -/// ``` -/// -/// The following example shows initialization of this logger and use of the log buffer. -/// ``` -/// use bp3d_logger::Logger; -/// use log::info; -/// use log::LevelFilter; -/// -/// fn main() { -/// let _guard = Logger::new().add_stdout().add_file("my-app").start(); -/// log::set_max_level(LevelFilter::Info); -/// bp3d_logger::enable_log_buffer(); // Enable log redirect pump into application channel. -/// //... application code with log redirect pump. -/// info!("Example message"); -/// let l = bp3d_logger::get_log_buffer().recv().unwrap();// Capture the last log message. -/// println!("Last log message: {}", l.msg); -/// bp3d_logger::disable_log_buffer(); -/// //... application code without log redirect pump. -/// } -/// ``` -pub struct Logger { - colors: Colors, - smart_stderr: bool, - std: Option, - file: Option, -} - -impl Default for Logger { - fn default() -> Self { - Self { - colors: Colors::default(), - smart_stderr: true, - std: None, - file: None - } - } -} - -impl Logger { - /// Creates a new instance of a logger builder. - pub fn new() -> Logger { - Logger::default() - } - - /// Sets the colors state when logging to stdout/stderr. - /// - /// The default behavior is to disable colors. - pub fn colors(mut self, state: Colors) -> Self { - self.colors = state; - self - } - - /// Enables or disables automatic redirection of error logs to stderr. - /// - /// The default for this flag is true. - pub fn smart_stderr(mut self, flag: bool) -> Self { - self.smart_stderr = flag; - self - } - - /// Enables stdout logging. - pub fn add_stdout(mut self) -> Self { - self.std = Some(backend::StdBackend::new(self.smart_stderr, self.colors)); - self - } - - /// Enables file logging to the given application. - /// - /// The application is given as a reference to [GetLogs](crate::GetLogs) to allow obtaining - /// a log directory from various sources. - /// - /// If the log directory could not be found the function prints an error to stderr. - pub fn add_file(mut self, app: T) -> Self { - if let Some(logs) = app.get_logs() { - self.file = Some(backend::FileBackend::new(logs)); - } else { - eprintln!("Failed to obtain application log directory"); - } - self - } - - /// Initializes the log implementation with this current configuration. - /// - /// NOTE: This returns a guard to flush all log buffers before returning. It is - /// necessary to flush log buffers because this implementation uses threads - /// to avoid blocking the main thread when issuing logs. - /// - /// NOTE 2: There are no safety concerns with running twice this function in the same - /// application, only that calling this function may be slow due to thread management. - pub fn start(self) -> Guard { - let _ = log::set_logger(&*BP3D_LOGGER); // Ignore the error - // (we can't do anything if there's already a logger set; - // unfortunately that is a limitation of the log crate) - - BP3D_LOGGER.start_new_thread(self); // Re-start the logging thread with the new configuration. - BP3D_LOGGER.enable(true); // Enable logging. - Guard - } - - /// Initializes the log implementation with this current configuration. - /// - /// NOTE: Since version 1.1.0 this is a redirect to bp3d_logger::with_logger. - #[deprecated(since = "1.1.0", note = "please use bp3d_logger::with_logger")] - pub fn run R>(self, f: F) -> R { - with_logger(self, f) - } -} - -/// Represents a logger guard. -/// -/// WARNING: Once this guard is dropped messages are no longer captured. -pub struct Guard; - -impl Drop for Guard { - fn drop(&mut self) { - // Disable the logger so further log requests are dropped. - BP3D_LOGGER.enable(false); - // Send termination command and join with logging thread. - BP3D_LOGGER.terminate(); - // Disable log buffer. - BP3D_LOGGER.enable_log_buffer(false); - // Clear by force all content of in memory log buffer. - BP3D_LOGGER.clear_log_buffer(); - } -} - -static BP3D_LOGGER: Lazy = Lazy::new(internal::LoggerImpl::new); - -/// Enables the log redirect pump. -pub fn enable_log_buffer() { - BP3D_LOGGER.enable_log_buffer(true); -} - -/// Disables the log redirect pump. -pub fn disable_log_buffer() { - BP3D_LOGGER.enable_log_buffer(false); - BP3D_LOGGER.clear_log_buffer(); -} - -/// Enables the stdout/stderr logger. -pub fn enable_stdout() { - ENABLE_STDOUT.store(true, Ordering::Release); -} - -/// Disables the stdout/stderr logger. -pub fn disable_stdout() { - ENABLE_STDOUT.store(false, Ordering::Release); -} - -/// Returns the buffer from the log redirect pump. -pub fn get_log_buffer() -> LogBuffer { - BP3D_LOGGER.get_log_buffer() -} - -/// Low-level log function. This injects log messages directly into the logging thread channel. -/// -/// This function applies basic formatting depending on the backend: -/// - For stdout/stderr backend the format is \[level\] msg -/// - For file backend the format is \[level\] msg and the message is recorded in the file -/// corresponding to the log target. -pub fn raw_log(msg: LogMsg) { - BP3D_LOGGER.low_level_log(msg) -} - -/// Shortcut to the flush command to avoid having to call behind the dyn interface. -pub fn flush() { - BP3D_LOGGER.flush(); -} - -/// Returns true if the logger is currently enabled and is capturing log messages. -pub fn enabled() -> bool { - BP3D_LOGGER.is_enabled() -} - -/// Runs a closure in scope of a logger configuration, then free the given logger configuration -/// and return closure result. -pub fn with_logger R>(logger: Logger, f: F) -> R { - let _guard = logger.start(); - f() -} +pub mod engine; +pub mod field; +pub mod logger; +pub mod profiler; +pub mod trace; +pub mod util; diff --git a/src/easy_termcolor.rs b/src/logger/interface.rs similarity index 58% rename from src/easy_termcolor.rs rename to src/logger/interface.rs index 5413a13..1c57dd8 100644 --- a/src/easy_termcolor.rs +++ b/src/logger/interface.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2021, BlockProject 3D +// Copyright (c) 2024, BlockProject 3D // // All rights reserved. // @@ -26,40 +26,49 @@ // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::fmt::Display; -use log::Level; -use termcolor::{Color, ColorSpec}; +use crate::field::Field; +use crate::logger::Level; +use crate::util::Location; +use std::fmt::Arguments; -pub struct EasyTermColor(pub T); +pub struct Callsite { + location: Location, + level: Level, +} -impl EasyTermColor { - pub fn write(mut self, elem: impl Display) -> Self { - let _ = write!(&mut self.0, "{}", elem); - self +impl Callsite { + pub const fn new(location: Location, level: Level) -> Self { + Self { location, level } } - pub fn color(mut self, elem: ColorSpec) -> Self { - let _ = self.0.set_color(&elem); - self + pub fn location(&self) -> &Location { + &self.location } - pub fn reset(mut self) -> Self { - let _ = self.0.reset(); - self + pub fn level(&self) -> Level { + self.level } +} - pub fn lf(mut self) -> Self { - let _ = writeln!(&mut self.0); - self - } +pub trait Logger { + fn log(&self, callsite: &'static Callsite, msg: Arguments, fields: &[Field]); } -pub fn color(level: Level) -> ColorSpec { - match level { - Level::Error => ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true).clone(), - Level::Warn => ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true).clone(), - Level::Info => ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true).clone(), - Level::Debug => ColorSpec::new().set_fg(Some(Color::Blue)).set_bold(true).clone(), - Level::Trace => ColorSpec::new().set_fg(Some(Color::Cyan)).set_bold(true).clone() +#[cfg(test)] +mod tests { + use crate::logger::Level; + use crate::{log, trace}; + + #[test] + fn api_test() { + let tuple = (41, 42); + let i = 42; + let b = true; + log!(Level::Info, { i }, "test: {i}: {}", i); + log!(Level::Error, "test: {}", i); + trace!({i} {?i} {id=i}, "test: {}", i); + trace!("test: {}, {}", i, i); + trace!("test41_42: {}, {}", tuple.0, tuple.1); + trace!({ b }, "a boolean"); } } diff --git a/src/logger/level.rs b/src/logger/level.rs new file mode 100644 index 0000000..dcd1105 --- /dev/null +++ b/src/logger/level.rs @@ -0,0 +1,79 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::fmt::{Display, Formatter}; + +/// An enum representing the available verbosity levels for a message. +#[repr(u8)] +#[derive(Clone, PartialEq, Copy, Ord, PartialOrd, Eq, Debug, Hash)] +pub enum Level { + /// The "trace" level. + /// + /// Designates very low priority, often extremely verbose, information. + Trace = 1, + + /// The "debug" level. + /// + /// Designates lower priority information. + Debug = 2, + + /// The "info" level. + /// + /// Designates useful information. + Info = 3, + + /// The "warn" level. + /// + /// Designates hazardous situations. + Warn = 4, + + /// The "error" level. + /// + /// Designates very serious errors. + // This way these line up with the discriminants for LevelFilter below + // This works because Rust treats field-less enums the same way as C does: + // https://doc.rust-lang.org/reference/items/enumerations.html#custom-discriminant-values-for-field-less-enumerations + Error = 5, +} + +static LOG_LEVEL_NAMES: [&str; 6] = ["OFF", "TRACE", "DEBUG", "INFO", "WARNING", "ERROR"]; + +impl Level { + /// Returns the string representation of the `Level`. + /// + /// This returns the same string as the `fmt::Display` implementation. + pub fn as_str(&self) -> &'static str { + LOG_LEVEL_NAMES[*self as usize] + } +} + +impl Display for Level { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} diff --git a/src/logger/macros.rs b/src/logger/macros.rs new file mode 100644 index 0000000..3cc9df8 --- /dev/null +++ b/src/logger/macros.rs @@ -0,0 +1,79 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[macro_export] +macro_rules! log { + ($level: expr, $({$($field: tt)*})*, $msg: literal $(,$($args: expr),*)?) => { + { + static _CALLSITE: $crate::logger::Callsite = $crate::logger::Callsite::new($crate::location!(), $level); + $crate::engine::get().log(&_CALLSITE, format_args!($msg $(, $($args),*)?), &[$($crate::field!($($field)*),)*]); + } + }; + ($level: expr, $msg: literal $(,$($args: expr),*)?) => { + { + static _CALLSITE: $crate::logger::Callsite = $crate::logger::Callsite::new($crate::location!(), $level); + $crate::engine::get().log(&_CALLSITE, format_args!($msg $(, $($args),*)?), &[]); + } + }; +} + +#[macro_export] +macro_rules! trace { + ($($args: tt)*) => { + #[cfg(debug_assertions)] + $crate::log!($crate::logger::Level::Trace, $($args)*); + }; +} + +#[macro_export] +macro_rules! debug { + ($($args: tt)*) => { + $crate::log!($crate::logger::Level::Debug, $($args)*); + }; +} + +#[macro_export] +macro_rules! info { + ($($args: tt)*) => { + $crate::log!($crate::logger::Level::Info, $($args)*); + }; +} + +#[macro_export] +macro_rules! warning { + ($($args: tt)*) => { + $crate::log!($crate::logger::Level::Warn, $($args)*); + }; +} + +#[macro_export] +macro_rules! error { + ($($args: tt)*) => { + $crate::log!($crate::logger::Level::Error, $($args)*); + }; +} diff --git a/src/logger/mod.rs b/src/logger/mod.rs new file mode 100644 index 0000000..7b3c642 --- /dev/null +++ b/src/logger/mod.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod interface; +mod level; +pub mod macros; + +pub use interface::*; +pub use level::Level; diff --git a/src/profiler/interface.rs b/src/profiler/interface.rs new file mode 100644 index 0000000..27364c7 --- /dev/null +++ b/src/profiler/interface.rs @@ -0,0 +1,36 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::field::Field; +use crate::profiler::section::Section; +use std::num::NonZeroU32; + +pub trait Profiler { + fn section_register(&self, section: &'static Section) -> NonZeroU32; + fn section_record(&self, id: NonZeroU32, start: u64, end: u64, fields: &[Field]); +} diff --git a/src/profiler/macros.rs b/src/profiler/macros.rs new file mode 100644 index 0000000..e6fbb35 --- /dev/null +++ b/src/profiler/macros.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[macro_export] +macro_rules! profiler_section_start { + ($name: ident $(: $parent: ident)?, $level: expr, $({$($field: tt)*})*) => { + static $name: $crate::profiler::section::Section = $crate::profiler::section::Section::new(stringify!($name), $crate::location!(), $level) + $(.set_parent(&$parent))?; + let _section = $name.enter($crate::field::FieldSet::new([$($crate::field!($($field)*),)*])); + }; + ($name: ident $(: $parent: ident)?, $level: expr) => { + static $name: $crate::profiler::section::Section = $crate::profiler::section::Section::new(stringify!($name), $crate::location!(), $level) + $(.set_parent(&$parent))?; + let _section = $name.enter($crate::field::FieldSet::new([])); + }; +} diff --git a/src/profiler/mod.rs b/src/profiler/mod.rs new file mode 100644 index 0000000..9261bc1 --- /dev/null +++ b/src/profiler/mod.rs @@ -0,0 +1,33 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod interface; +mod macros; +pub mod section; + +pub use interface::*; diff --git a/src/profiler/section.rs b/src/profiler/section.rs new file mode 100644 index 0000000..a10baad --- /dev/null +++ b/src/profiler/section.rs @@ -0,0 +1,157 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::field::FieldSet; +use crate::util::Location; +use std::num::NonZeroU32; +use std::sync::OnceLock; +use std::time::Instant; + +#[repr(u8)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Level { + /// A section located in a critically hot path. + Critical = 0, + + /// A periodic section. + Periodic = 1, + + // An event based section. + Event = 2, +} + +thread_local! { + static CUR_TIME: Instant = Instant::now(); +} + +pub struct Entered<'a, const N: usize> { + id: NonZeroU32, + start: u64, + fields: FieldSet<'a, N>, +} + +impl Drop for Entered<'_, N> { + fn drop(&mut self) { + let end = CUR_TIME.with(|v| v.elapsed().as_nanos() as _); + crate::engine::get().section_record(self.id, self.start, end, self.fields.as_ref()); + } +} + +pub struct Section { + name: &'static str, + location: Location, + level: Level, + parent: Option<&'static Section>, + id: OnceLock, +} + +impl Section { + pub const fn new(name: &'static str, location: Location, level: Level) -> Self { + Self { + name, + location, + level, + parent: None, + id: OnceLock::new(), + } + } + + pub const fn set_parent(mut self, parent: &'static Section) -> Self { + self.parent = Some(parent); + self + } + + pub fn name(&self) -> &'static str { + self.name + } + + pub fn location(&self) -> &Location { + &self.location + } + + pub fn level(&self) -> Level { + self.level + } + + pub fn parent(&self) -> Option<&'static Section> { + self.parent + } + + pub fn get_id(&'static self) -> &'static NonZeroU32 { + self.id + .get_or_init(|| crate::engine::get().section_register(self)) + } + + pub fn enter<'a, const N: usize>(&'static self, fields: FieldSet<'a, N>) -> Entered<'a, N> { + let id = self.get_id(); + Entered { + id: *id, + start: CUR_TIME.with(|v| v.elapsed().as_nanos() as _), + fields, + } + } +} + +#[cfg(test)] +mod tests { + use crate::field::FieldSet; + use crate::profiler::section::{Level, Section}; + use crate::{fields, location, profiler_section_start}; + + #[test] + fn basic() { + static _SECTION: Section = Section::new("api_test", location!(), Level::Event); + } + + #[test] + fn api_test() { + static SECTION: Section = Section::new("api_test", location!(), Level::Event); + static _SECTION2: Section = + Section::new("api_test2", location!(), Level::Event).set_parent(&SECTION); + SECTION.enter(FieldSet::new(fields!())); + SECTION.enter(FieldSet::new(fields!({ test = 42 }))); + SECTION.enter(FieldSet::new(fields!({ test = "test 123" }))); + SECTION.enter(FieldSet::new(fields!({ test = 42.42 }))); + SECTION.enter(FieldSet::new(fields!({test=?Level::Event}))); + SECTION.enter(FieldSet::new(fields!({test=?Level::Event} {test2=42}))); + let value = 32; + let str = "this is a test"; + let lvl = Level::Event; + SECTION.enter(FieldSet::new(fields!({value} {str} {?lvl} {test = value}))); + } + + #[test] + fn api_test2() { + let value = 32; + let str = "this is a test"; + let lvl = Level::Event; + profiler_section_start!(API_TEST, Level::Event); + profiler_section_start!(API2_TEST: API_TEST, Level::Event); + profiler_section_start!(API3_TEST_WITH_PARAMS: API2_TEST, Level::Event, {value} {str} {?lvl} {test=value}); + } +} diff --git a/src/trace/future.rs b/src/trace/future.rs new file mode 100644 index 0000000..17b9b79 --- /dev/null +++ b/src/trace/future.rs @@ -0,0 +1,66 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::trace::span::{Entered, Span}; +use crate::trace::Trace; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +pub struct TracedFuture { + future: F, + span: Option, +} + +impl Future for TracedFuture { + type Output = F::Output; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + unsafe { + let pin = Pin::new_unchecked(&mut self.future); + let value = pin.poll(cx); + if value.is_ready() { + drop(self.span.take()); + } + value + } + } +} + +impl Unpin for TracedFuture {} + +impl Trace for F { + type Output = TracedFuture; + + fn trace(self, span: Span) -> Self::Output { + TracedFuture { + future: self, + span: Some(span.enter()), + } + } +} diff --git a/src/trace/interface.rs b/src/trace/interface.rs new file mode 100644 index 0000000..51adc36 --- /dev/null +++ b/src/trace/interface.rs @@ -0,0 +1,45 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::field::Field; +use crate::trace::span::{Callsite, Id, Span}; +use std::num::NonZeroU32; + +pub trait Tracer { + fn register_callsite(&self, callsite: &'static Callsite) -> NonZeroU32; + fn span_create(&self, callsite: NonZeroU32, fields: &[Field]) -> NonZeroU32; + fn span_enter(&self, id: Id); + fn span_record(&self, id: Id, fields: &[Field]); + fn span_exit(&self, id: Id); + fn span_destroy(&self, id: Id); +} + +pub trait Trace { + type Output; + fn trace(self, span: Span) -> Self::Output; +} diff --git a/src/trace/macros.rs b/src/trace/macros.rs new file mode 100644 index 0000000..fea966a --- /dev/null +++ b/src/trace/macros.rs @@ -0,0 +1,45 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[macro_export] +macro_rules! span { + ($name: ident, $({$($field: tt)*})*) => { + { + static $name: $crate::trace::span::Callsite = + $crate::trace::span::Callsite::new(stringify!($name), $crate::location!()); + $crate::trace::span::Span::with_fields(&$name, &[$($crate::field!($($field)*),)*]) + } + }; + ($name: ident) => { + { + static $name: $crate::trace::span::Callsite = + $crate::trace::span::Callsite::new(stringify!($name), $crate::location!()); + $crate::trace::span::Span::new(&$name) + } + }; +} diff --git a/src/trace/mod.rs b/src/trace/mod.rs new file mode 100644 index 0000000..0649c51 --- /dev/null +++ b/src/trace/mod.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod future; +mod interface; +mod macros; +pub mod span; + +pub use interface::*; diff --git a/src/trace/span.rs b/src/trace/span.rs new file mode 100644 index 0000000..fa02a0a --- /dev/null +++ b/src/trace/span.rs @@ -0,0 +1,152 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::field::Field; +use crate::util::Location; +use std::num::{NonZeroU32, NonZeroU64}; +use std::sync::OnceLock; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[repr(transparent)] +pub struct Id(NonZeroU64); + +impl Id { + pub fn new(callsite: NonZeroU32, instance: NonZeroU32) -> Self { + Self(unsafe { + NonZeroU64::new_unchecked((callsite.get() as u64) << 32 | instance.get() as u64) + }) + } + + pub fn from_raw(id: NonZeroU64) -> Self { + Self(id) + } + + pub fn into_raw(self) -> NonZeroU64 { + self.0 + } + + pub fn get_callsite(&self) -> NonZeroU32 { + unsafe { NonZeroU32::new_unchecked((self.0.get() >> 32) as u32) } + } + + pub fn get_instance(&self) -> NonZeroU32 { + unsafe { NonZeroU32::new_unchecked(self.0.get() as u32) } + } +} + +pub struct Callsite { + name: &'static str, + location: Location, + id: OnceLock, +} + +impl Callsite { + pub const fn new(name: &'static str, location: Location) -> Self { + Self { + name, + location, + id: OnceLock::new(), + } + } + + pub fn location(&self) -> &Location { + &self.location + } + + pub fn name(&self) -> &'static str { + self.name + } + + pub fn get_id(&'static self) -> &'static NonZeroU32 { + self.id + .get_or_init(|| crate::engine::get().register_callsite(self)) + } +} + +pub struct Entered { + id: Id, +} + +impl Drop for Entered { + fn drop(&mut self) { + crate::engine::get().span_exit(self.id); + } +} + +pub struct Span { + id: Id, +} + +impl Span { + pub fn with_fields(callsite: &'static Callsite, fields: &[Field]) -> Self { + let callsite = *callsite.get_id(); + let instance = crate::engine::get().span_create(callsite, fields); + Self { + id: Id::new(callsite, instance), + } + } + + pub fn new(callsite: &'static Callsite) -> Self { + let callsite = *callsite.get_id(); + let instance = crate::engine::get().span_create(callsite, &[]); + Self { + id: Id::new(callsite, instance), + } + } + + pub fn record(&self, fields: &[Field]) { + crate::engine::get().span_record(self.id, fields); + } + + pub fn enter(&self) -> Entered { + Entered { id: self.id } + } +} + +impl Drop for Span { + fn drop(&mut self) { + crate::engine::get().span_destroy(self.id); + } +} + +#[cfg(test)] +mod tests { + use crate::profiler::section::Level; + use crate::{fields, span}; + + #[test] + fn api_test() { + let value = 32; + let str = "this is a test"; + let lvl = Level::Event; + let _span = span!(API_TEST); + let span = span!(API_TEST2, {value} {str} {?lvl} {test=value}); + span.record(fields!({ test2 = str }).as_ref()); + let _entered = span.enter(); + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..269ac62 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,100 @@ +// Copyright (c) 2024, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// Extracts the target name and the module path (without the target name) from a full module path string. +/// +/// # Arguments +/// +/// * `base_string`: a full module path string (ex: bp3d_logger::util::extract_target_module). +/// +/// returns: (&str, &str) +pub fn extract_target_module(base_string: &str) -> (&str, &str) { + let target = base_string + .find("::") + .map(|v| &base_string[..v]) + .unwrap_or(base_string); + let module = base_string.find("::").map(|v| &base_string[(v + 2)..]); + (target, module.unwrap_or("main")) +} + +/// The context of a log message. +#[derive(Clone, Copy, Debug)] +pub struct Location { + module_path: &'static str, + file: &'static str, + line: u32, +} + +impl Location { + /// Creates a new instance of a log message location. + /// + /// This function is const to let the caller store location structures in statics. + /// + /// # Arguments + /// + /// * `module_path`: the module path obtained from the [module_path](module_path) macro. + /// * `file`: the source file obtained from the [file](file) macro. + /// * `line`: the line number in the source file obtained from the [line](line) macro. + /// + /// returns: Metadata + pub const fn new(module_path: &'static str, file: &'static str, line: u32) -> Self { + Self { + module_path, + file, + line, + } + } + + /// The module path which issued this log message. + pub fn module_path(&self) -> &'static str { + self.module_path + } + + /// The source file which issued this log message. + pub fn file(&self) -> &'static str { + self.file + } + + /// The line in the source file which issued this log message. + pub fn line(&self) -> u32 { + self.line + } + + /// Extracts the target name and the module name from the module path. + pub fn get_target_module(&self) -> (&'static str, &'static str) { + extract_target_module(self.module_path) + } +} + +/// Generate a [Location](crate::Location) structure. +#[macro_export] +macro_rules! location { + () => { + $crate::util::Location::new(module_path!(), file!(), line!()) + }; +}