diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 69788753..c5010d9c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -75,6 +75,17 @@ jobs: - run: cargo clippy --lib --tests --target wasm32-wasi working-directory: ./lib + clippy_wasm_unknown: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: true + - run: rustup update && rustup target add wasm32-unknown-unknown && rustup component add clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy --lib --tests --target wasm32-unknown-unknown --features getrandom/custom --features oxsdatatypes/custom-now + working-directory: ./lib + clippy_msv: runs-on: ubuntu-latest steps: @@ -158,7 +169,7 @@ jobs: submodules: true - run: rustup update - uses: Swatinem/rust-cache@v2 - - run: cargo test --all-features + - run: cargo test env: RUST_BACKTRACE: 1 @@ -185,7 +196,7 @@ jobs: - run: rustup update - uses: Swatinem/rust-cache@v2 - run: Remove-Item -LiteralPath "C:\msys64\" -Force -Recurse - - run: cargo test --all-features + - run: cargo test env: RUST_BACKTRACE: 1 diff --git a/js/Cargo.toml b/js/Cargo.toml index ee103f79..86390266 100644 --- a/js/Cargo.toml +++ b/js/Cargo.toml @@ -14,7 +14,7 @@ crate-type = ["cdylib"] name = "oxigraph" [dependencies] -oxigraph = { version = "0.4.0-alpha.1-dev", path="../lib" } +oxigraph = { version = "0.4.0-alpha.1-dev", path="../lib", features = ["js"] } wasm-bindgen = "0.2" js-sys = "0.3" console_error_panic_hook = "0.1" diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 1ea58475..d9be7aed 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -16,6 +16,7 @@ rust-version = "1.65" [features] default = [] +js = ["getrandom/js", "oxsdatatypes/js", "js-sys"] http_client = ["oxhttp", "oxhttp/rustls"] rocksdb_debug = [] @@ -46,8 +47,8 @@ oxrocksdb-sys = { version = "0.4.0-alpha.1-dev", path="../oxrocksdb-sys" } oxhttp = { version = "0.1", optional = true } [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] -getrandom = { version = "0.2", features = ["js"] } -js-sys = "0.3" +getrandom = "0.2" +js-sys = { version = "0.3", optional = true } [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] criterion = "0.5" diff --git a/lib/oxsdatatypes/Cargo.toml b/lib/oxsdatatypes/Cargo.toml index 1535a325..f3a1e3c3 100644 --- a/lib/oxsdatatypes/Cargo.toml +++ b/lib/oxsdatatypes/Cargo.toml @@ -13,8 +13,12 @@ An implementation of some XSD datatypes for SPARQL implementations edition = "2021" rust-version = "1.65" +[features] +js = ["js-sys"] +custom-now = [] + [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] -js-sys = "0.3" +js-sys = { version = "0.3", optional = true } [package.metadata.docs.rs] all-features = true diff --git a/lib/oxsdatatypes/README.md b/lib/oxsdatatypes/README.md index 1f0b419a..a164f282 100644 --- a/lib/oxsdatatypes/README.md +++ b/lib/oxsdatatypes/README.md @@ -32,6 +32,22 @@ Each datatype provides: * `from_be_bytes` and `to_be_bytes` methods for serialization. +### `DateTime::now` behavior + +The `DateTime::now()` function needs special OS support. +Currently: +- If the `custom-now` feature is enabled, a function computing `now` must be set: + ```rust + use oxsdatatypes::{DateTimeError, Duration}; + + #[no_mangle] + fn custom_ox_now() -> Result { + unimplemented!("now implementation") + } + ``` +- For `wasm32-unknown-unknown` if the `js` feature is enabled the `Date.now()` ECMAScript API is used. +- For all other targets `SystemTime::now()` is used. + ## License This project is licensed under either of diff --git a/lib/oxsdatatypes/src/date_time.rs b/lib/oxsdatatypes/src/date_time.rs index b5cc8d9e..0bfe29d1 100644 --- a/lib/oxsdatatypes/src/date_time.rs +++ b/lib/oxsdatatypes/src/date_time.rs @@ -191,7 +191,7 @@ impl DateTime { pub fn checked_sub_day_time_duration(self, rhs: impl Into) -> Option { let rhs = rhs.into(); Some(Self { - timestamp: self.timestamp.checked_sub_seconds(rhs.all_seconds())?, + timestamp: self.timestamp.checked_sub_seconds(rhs.as_seconds())?, }) } @@ -1757,7 +1757,22 @@ impl Timestamp { } } -#[cfg(all(target_family = "wasm", target_os = "unknown"))] +#[cfg(feature = "custom-now")] +#[allow(unsafe_code)] +pub fn since_unix_epoch() -> Result { + extern "Rust" { + fn custom_ox_now() -> Result; + } + + unsafe { custom_ox_now() } +} + +#[cfg(all( + feature = "js", + not(feature = "custom-now"), + target_family = "wasm", + target_os = "unknown" +))] fn since_unix_epoch() -> Result { Ok(Duration::new( 0, @@ -1766,7 +1781,10 @@ fn since_unix_epoch() -> Result { )) } -#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] +#[cfg(not(any( + feature = "custom-now", + all(feature = "js", target_family = "wasm", target_os = "unknown") +)))] fn since_unix_epoch() -> Result { use std::time::SystemTime; @@ -2605,6 +2623,17 @@ mod tests { Ok(()) } + #[cfg(feature = "custom-now")] + #[test] + fn custom_now() { + #[no_mangle] + fn custom_ox_now() -> Result { + Ok(Duration::default()) + } + assert!(DateTime::now().is_ok()); + } + + #[cfg(not(feature = "custom-now"))] #[test] fn now() -> Result<(), XsdParseError> { let now = DateTime::now().unwrap(); diff --git a/lib/oxsdatatypes/src/duration.rs b/lib/oxsdatatypes/src/duration.rs index 4815eb16..27ce5d97 100644 --- a/lib/oxsdatatypes/src/duration.rs +++ b/lib/oxsdatatypes/src/duration.rs @@ -85,7 +85,7 @@ impl Duration { #[inline] #[must_use] pub(super) const fn all_seconds(self) -> Decimal { - self.day_time.all_seconds() + self.day_time.as_seconds() } #[inline] @@ -443,8 +443,9 @@ impl DayTimeDuration { self.seconds.checked_rem(60).unwrap() } + /// The duration in seconds. #[inline] - pub(super) const fn all_seconds(self) -> Decimal { + pub const fn as_seconds(self) -> Decimal { self.seconds } diff --git a/lib/src/sparql/algebra.rs b/lib/src/sparql/algebra.rs index 41fad114..f468e69e 100644 --- a/lib/src/sparql/algebra.rs +++ b/lib/src/sparql/algebra.rs @@ -6,10 +6,10 @@ use crate::model::*; use crate::sparql::eval::Timer; +use oxsdatatypes::DayTimeDuration; use spargebra::GraphUpdateOperation; use std::fmt; use std::str::FromStr; -use std::time::Duration; /// A parsed [SPARQL query](https://www.w3.org/TR/sparql11-query/). /// @@ -32,7 +32,7 @@ use std::time::Duration; pub struct Query { pub(super) inner: spargebra::Query, pub(super) dataset: QueryDataset, - pub(super) parsing_duration: Option, + pub(super) parsing_duration: Option, } impl Query { @@ -43,7 +43,7 @@ impl Query { Ok(Self { dataset: query.dataset, inner: query.inner, - parsing_duration: Some(start.elapsed()), + parsing_duration: start.elapsed(), }) } diff --git a/lib/src/sparql/eval.rs b/lib/src/sparql/eval.rs index 82f0e06f..90a52efa 100644 --- a/lib/src/sparql/eval.rs +++ b/lib/src/sparql/eval.rs @@ -35,9 +35,6 @@ use std::hash::{Hash, Hasher}; use std::iter::Iterator; use std::iter::{empty, once}; use std::rc::Rc; -use std::time::Duration as StdDuration; -#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] -use std::time::Instant; use std::{fmt, io, str}; const REGEX_SIZE_LIMIT: usize = 1_000_000; @@ -258,16 +255,19 @@ impl SimpleEvaluator { label: eval_node_label(pattern), children: stat_children, exec_count: Cell::new(0), - exec_duration: Cell::new(StdDuration::from_secs(0)), + exec_duration: Cell::new(self.run_stats.then(DayTimeDuration::default)), }); if self.run_stats { let stats = Rc::clone(&stats); evaluator = Rc::new(move |tuple| { let start = Timer::now(); let inner = evaluator(tuple); - stats - .exec_duration - .set(stats.exec_duration.get() + start.elapsed()); + stats.exec_duration.set( + stats + .exec_duration + .get() + .and_then(|stat| stat.checked_add(start.elapsed()?)), + ); Box::new(StatsIterator { inner, stats: Rc::clone(&stats), @@ -5648,9 +5648,12 @@ impl Iterator for StatsIterator { fn next(&mut self) -> Option> { let start = Timer::now(); let result = self.inner.next(); - self.stats - .exec_duration - .set(self.stats.exec_duration.get() + start.elapsed()); + self.stats.exec_duration.set( + self.stats + .exec_duration + .get() + .and_then(|stat| stat.checked_add(start.elapsed()?)), + ); if matches!(result, Some(Ok(_))) { self.stats.exec_count.set(self.stats.exec_count.get() + 1); } @@ -5662,7 +5665,7 @@ pub struct EvalNodeWithStats { pub label: String, pub children: Vec>, pub exec_count: Cell, - pub exec_duration: Cell, + pub exec_duration: Cell>, } impl EvalNodeWithStats { @@ -5677,10 +5680,10 @@ impl EvalNodeWithStats { if with_stats { writer.write_event(JsonEvent::ObjectKey("number of results"))?; writer.write_event(JsonEvent::Number(&self.exec_count.get().to_string()))?; - writer.write_event(JsonEvent::ObjectKey("duration in seconds"))?; - writer.write_event(JsonEvent::Number( - &self.exec_duration.get().as_secs_f32().to_string(), - ))?; + if let Some(duration) = self.exec_duration.get() { + writer.write_event(JsonEvent::ObjectKey("duration in seconds"))?; + writer.write_event(JsonEvent::Number(&duration.as_seconds().to_string()))?; + } } writer.write_event(JsonEvent::ObjectKey("children"))?; writer.write_event(JsonEvent::StartArray)?; @@ -5696,9 +5699,12 @@ impl fmt::Debug for EvalNodeWithStats { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut obj = f.debug_struct("Node"); obj.field("name", &self.label); - if self.exec_duration.get() > StdDuration::default() { + if let Some(exec_duration) = self.exec_duration.get() { obj.field("number of results", &self.exec_count.get()); - obj.field("duration in seconds", &self.exec_duration.get()); + obj.field( + "duration in seconds", + &f32::from(Float::from(exec_duration.as_seconds())), + ); } if !self.children.is_empty() { obj.field("children", &self.children); @@ -5844,39 +5850,19 @@ fn format_list(values: impl IntoIterator) -> String { .join(", ") } -#[cfg(all(target_family = "wasm", target_os = "unknown"))] -pub struct Timer { - timestamp_ms: f64, -} - -#[cfg(all(target_family = "wasm", target_os = "unknown"))] -impl Timer { - pub fn now() -> Self { - Self { - timestamp_ms: js_sys::Date::now(), - } - } - - pub fn elapsed(&self) -> StdDuration { - StdDuration::from_secs_f64((js_sys::Date::now() - self.timestamp_ms) / 1000.) - } -} - -#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] pub struct Timer { - instant: Instant, + start: Option, } -#[cfg(not(all(target_family = "wasm", target_os = "unknown")))] impl Timer { pub fn now() -> Self { Self { - instant: Instant::now(), + start: DateTime::now().ok(), } } - pub fn elapsed(&self) -> StdDuration { - self.instant.elapsed() + pub fn elapsed(&self) -> Option { + DateTime::now().ok()?.checked_sub(self.start?) } } diff --git a/lib/src/sparql/mod.rs b/lib/src/sparql/mod.rs index d5e7233e..95249203 100644 --- a/lib/src/sparql/mod.rs +++ b/lib/src/sparql/mod.rs @@ -23,6 +23,7 @@ pub(crate) use crate::sparql::update::evaluate_update; use crate::storage::StorageReader; use json_event_parser::{JsonEvent, JsonWriter}; pub use oxrdf::{Variable, VariableNameParseError}; +use oxsdatatypes::{DayTimeDuration, Float}; pub use sparesults::QueryResultsFormat; pub use spargebra::ParseError; use sparopt::algebra::GraphPattern; @@ -272,8 +273,8 @@ impl From for UpdateOptions { pub struct QueryExplanation { inner: Rc, with_stats: bool, - parsing_duration: Option, - planning_duration: Duration, + parsing_duration: Option, + planning_duration: Option, } impl QueryExplanation { @@ -284,13 +285,15 @@ impl QueryExplanation { if let Some(parsing_duration) = self.parsing_duration { writer.write_event(JsonEvent::ObjectKey("parsing duration in seconds"))?; writer.write_event(JsonEvent::Number( - &parsing_duration.as_secs_f32().to_string(), + &parsing_duration.as_seconds().to_string(), + ))?; + } + if let Some(planning_duration) = self.planning_duration { + writer.write_event(JsonEvent::ObjectKey("planning duration in seconds"))?; + writer.write_event(JsonEvent::Number( + &planning_duration.as_seconds().to_string(), ))?; } - writer.write_event(JsonEvent::ObjectKey("planning duration in seconds"))?; - writer.write_event(JsonEvent::Number( - &self.planning_duration.as_secs_f32().to_string(), - ))?; writer.write_event(JsonEvent::ObjectKey("plan"))?; self.inner.json_node(&mut writer, self.with_stats)?; writer.write_event(JsonEvent::EndObject) @@ -299,6 +302,20 @@ impl QueryExplanation { impl fmt::Debug for QueryExplanation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self.inner) + let mut obj = f.debug_struct("QueryExplanation"); + if let Some(parsing_duration) = self.parsing_duration { + obj.field( + "parsing duration in seconds", + &f32::from(Float::from(parsing_duration.as_seconds())), + ); + } + if let Some(planning_duration) = self.planning_duration { + obj.field( + "planning duration in seconds", + &f32::from(Float::from(planning_duration.as_seconds())), + ); + } + obj.field("tree", &self.inner); + obj.finish() } }