diff --git a/js/optify-config/README.md b/js/optify-config/README.md index 18badf0e..da864fd1 100644 --- a/js/optify-config/README.md +++ b/js/optify-config/README.md @@ -62,6 +62,16 @@ yarn build:ts yarn test ``` +## Benchmarking + +Run: +```shell +rm -rf target config.*.node +yarn build +yarn build:ts +node benchmarks/get_all_options.mjs +``` + ## Formatting To automatically change the Rust code, run: diff --git a/js/optify-config/benchmarks/get_all_options.mjs b/js/optify-config/benchmarks/get_all_options.mjs new file mode 100644 index 00000000..f9d770cd --- /dev/null +++ b/js/optify-config/benchmarks/get_all_options.mjs @@ -0,0 +1,78 @@ +import { Bench } from "tinybench"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Import the native module +const { OptionsProvider, GetOptionsPreferences } = await import( + "../dist/index.js" +); + +const configurableConfigsPath = path.join( + __dirname, + "../../../tests/test_suites/configurable_values/configs" +); + +const provider = OptionsProvider.build(configurableConfigsPath); + +const preferences = new GetOptionsPreferences(); +preferences.enableConfigurableStrings(); + +const featureTrials = [ + ["simple"], + ["simple", "imports"], + ["imports_imports"], + ["simple", "override_name"], + ["simple", "raw_overrides"], + ["simple", "with_files"], + ["simple", "with_files_in_arguments"], + ["simple", "complex_deep_merge"], + ["simple", "complex_wide_structure"], + [ + "simple", + "complex_deep_merge", + "complex_nested_objects", + "complex_wide_structure", + ], +]; + +const WARMUP_ITERATIONS = 20; +const ITERATIONS = 5000; + +console.log( + "Benchmarking getAllOptions vs JSON.parse(getAllOptionsJson(...))\n" +); +console.log("=".repeat(80)); + +for (const features of featureTrials) { + const featureLabel = features.join(", "); + + // Warmup both methods + for (let i = 0; i < WARMUP_ITERATIONS; ++i) { + provider.getAllOptions(features, preferences); + JSON.parse(provider.getAllOptionsJson(features, preferences)); + } + + const bench = new Bench({ + time: 3000, + iterations: ITERATIONS, + }); + + bench + .add(`getAllOptions`, () => { + provider.getAllOptions(features, preferences); + }) + .add(`JSON.parse(getAllOptionsJson)`, () => { + JSON.parse(provider.getAllOptionsJson(features, preferences)); + }); + + await bench.run(); + + console.log(`\nFeatures: [${featureLabel}]`); + console.log("-".repeat(80)); + console.table(bench.table()); +} + +console.log("\n" + "=".repeat(80)); +console.log("Benchmark complete."); diff --git a/js/optify-config/package.json b/js/optify-config/package.json index b0067ec9..d014aa6d 100644 --- a/js/optify-config/package.json +++ b/js/optify-config/package.json @@ -35,6 +35,7 @@ "@types/mocha": "^10.0.10", "corepack": "^0.32.0", "jest": "^29.7.0", + "tinybench": "^5.1.0", "ts-jest": "^29.3.4", "typescript": "^5.8.3" }, diff --git a/js/optify-config/src/convert.rs b/js/optify-config/src/convert.rs new file mode 100644 index 00000000..4bb92af0 --- /dev/null +++ b/js/optify-config/src/convert.rs @@ -0,0 +1,36 @@ +#![deny(clippy::all)] + +use napi::Env; + +pub fn convert_to_js(env: Env, value: &serde_json::Value) -> napi::JsUnknown { + match value { + serde_json::Value::Null => env.get_null().unwrap().into_unknown(), + serde_json::Value::Bool(b) => env.get_boolean(*b).unwrap().into_unknown(), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + env.create_int64(i).unwrap().into_unknown() + } else if let Some(f) = n.as_f64() { + env.create_double(f).unwrap().into_unknown() + } else { + env.get_null().unwrap().into_unknown() + } + } + serde_json::Value::String(s) => env.create_string(s).unwrap().into_unknown(), + serde_json::Value::Array(arr) => { + let mut js_array = env.create_array_with_length(arr.len()).unwrap(); + for (i, item) in arr.iter().enumerate() { + js_array + .set_element(i as u32, convert_to_js(env, item)) + .unwrap(); + } + js_array.into_unknown() + } + serde_json::Value::Object(map) => { + let mut js_obj = env.create_object().unwrap(); + for (key, val) in map.iter() { + js_obj.set(key, convert_to_js(env, val)).unwrap(); + } + js_obj.into_unknown() + } + } +} diff --git a/js/optify-config/src/lib.rs b/js/optify-config/src/lib.rs index 68fc7557..e3e09d50 100644 --- a/js/optify-config/src/lib.rs +++ b/js/optify-config/src/lib.rs @@ -1,5 +1,6 @@ #![deny(clippy::all)] +mod convert; mod metadata; mod preferences; mod provider; diff --git a/js/optify-config/src/provider.rs b/js/optify-config/src/provider.rs index b5c646a6..887c2e02 100644 --- a/js/optify-config/src/provider.rs +++ b/js/optify-config/src/provider.rs @@ -1,8 +1,10 @@ #![deny(clippy::all)] +use napi::Env; use optify::builder::{OptionsProviderBuilder, OptionsRegistryBuilder}; use optify::provider::{OptionsProvider, OptionsRegistry}; +use crate::convert::convert_to_js; use crate::metadata::{to_js_options_metadata, JsOptionsMetadata}; use crate::preferences::JsGetOptionsPreferences; @@ -58,6 +60,37 @@ impl JsOptionsProvider { .collect() } + /// Gets all options for the specified feature names. + /// Should return a JavaScript object. + /// + /// There normally isn't much of a performance difference between using + /// `JSON.parse(get_all_options_json(...))` and `get_all_options(...)`. + /// Large JSON objects with over 50 keys may be slightly slower with `get_all_options(...)`. + #[napi] + pub fn get_all_options( + &self, + env: Env, + feature_names: Vec, + preferences: Option<&JsGetOptionsPreferences>, + ) -> napi::Result { + let preferences = preferences.map(|p| &p.inner); + match self + .inner + .as_ref() + .unwrap() + .get_all_options(&feature_names, None, preferences) + { + Ok(options) => Ok(convert_to_js(env, &options)), + Err(e) => Err(napi::Error::from_reason(e.to_string())), + } + } + + /// Gets all options for the specified feature names. + /// Returns a string which can be parsed as JSON to get the options. + /// + /// There normally isn't much of a performance difference between using + /// `JSON.parse(get_all_options_json(...))` and `get_all_options(...)`. + /// Large JSON objects with over 50 keys may be slightly slower with `get_all_options(...)`. #[napi] pub fn get_all_options_json( &self, diff --git a/js/optify-config/src/watcher.rs b/js/optify-config/src/watcher.rs index fc293507..1cf3b642 100644 --- a/js/optify-config/src/watcher.rs +++ b/js/optify-config/src/watcher.rs @@ -1,9 +1,11 @@ #![deny(clippy::all)] +use napi::Env; use optify::builder::{OptionsRegistryBuilder, OptionsWatcherBuilder}; use optify::provider::{OptionsRegistry, OptionsWatcher}; use std::sync::Arc; +use crate::convert::convert_to_js; use crate::metadata::{to_js_options_metadata, JsOptionsMetadata}; use crate::preferences::JsGetOptionsPreferences; use crate::watcher_options::JsWatcherOptions; @@ -119,7 +121,37 @@ impl JsOptionsWatcher { .map(|(k, v)| (k, to_js_options_metadata(v))) .collect() } + /// Gets all options for the specified feature names. + /// Should return a JavaScript object. + /// + /// There normally isn't much of a performance difference between using + /// `JSON.parse(get_all_options_json(...))` and `get_all_options(...)`. + /// Large JSON objects with over 50 keys may be slightly slower with `get_all_options(...)`. + #[napi] + pub fn get_all_options( + &self, + env: Env, + feature_names: Vec, + preferences: Option<&JsGetOptionsPreferences>, + ) -> napi::Result { + let preferences = preferences.map(|p| &p.inner); + match self + .inner + .as_ref() + .unwrap() + .get_all_options(&feature_names, None, preferences) + { + Ok(options) => Ok(convert_to_js(env, &options)), + Err(e) => Err(napi::Error::from_reason(e.to_string())), + } + } + /// Gets all options for the specified feature names. + /// Returns a string which can be parsed as JSON to get the options. + /// + /// There normally isn't much of a performance difference between using + /// `JSON.parse(get_all_options_json(...))` and `get_all_options(...)`. + /// Large JSON objects with over 50 keys may be slightly slower with `get_all_options(...)`. #[napi] pub fn get_all_options_json( &self, diff --git a/js/optify-config/tests/provider.test.ts b/js/optify-config/tests/provider.test.ts index 2dda3053..96f17778 100644 --- a/js/optify-config/tests/provider.test.ts +++ b/js/optify-config/tests/provider.test.ts @@ -33,6 +33,9 @@ describe('Provider', () => { const options = JSON.parse(provider.getAllOptionsJson(['feature_A'])) const expectedOptions = JSON.parse(fs.readFileSync(path.join(configsPath, 'feature_A.json'), 'utf8'))['options'] expect(options).toEqual(expectedOptions) + + const optionsObj = provider.getAllOptions(['feature_A']) + expect(optionsObj).toEqual(expectedOptions) }) test(`${name} get_all_options_json A and B`, () => { diff --git a/js/optify-config/yarn.lock b/js/optify-config/yarn.lock index c589f0e0..c8832a83 100644 --- a/js/optify-config/yarn.lock +++ b/js/optify-config/yarn.lock @@ -738,6 +738,7 @@ __metadata: "@types/mocha": "npm:^10.0.10" corepack: "npm:^0.32.0" jest: "npm:^29.7.0" + tinybench: "npm:^5.1.0" ts-jest: "npm:^29.3.4" typescript: "npm:^5.8.3" languageName: unknown @@ -3276,6 +3277,13 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^5.1.0": + version: 5.1.0 + resolution: "tinybench@npm:5.1.0" + checksum: 10c0/a9d53204888711c6e67d374b96b9a8a096aac80dda755562de3ed7afa2a39f04abd3cd2a64e401a3f0ece4d49ae20230be0986c733030756bde2f0d56e78cd0f + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.12": version: 0.2.14 resolution: "tinyglobby@npm:0.2.14" diff --git a/ruby/optify/benchmarks/get_options.rb b/ruby/optify/benchmarks/get_options.rb index 9bced58b..9534c2cd 100644 --- a/ruby/optify/benchmarks/get_options.rb +++ b/ruby/optify/benchmarks/get_options.rb @@ -17,10 +17,8 @@ feature_b = simple_provider.get_canonical_feature_name('b') simple_feature_trials = [ - ['a'], [feature_a], ['a', feature_a, 'b', feature_b], - ['a', feature_a, 'b', feature_b, 'A_with_comments', 'a', 'B'], ['a', feature_a, 'b', feature_b, 'A_with_comments', 'a', 'B', 'a', feature_a, 'b', feature_b, 'A_with_comments', 'a', 'B', 'a', feature_a, 'b', feature_b, 'A_with_comments', 'a', 'B'] ] @@ -37,17 +35,11 @@ ['imports_imports'], %w[simple override_name], %w[simple raw_overrides], - ['with_files'], %w[simple with_files], - ['with_files_in_arguments'], %w[simple with_files_in_arguments], - ['complex_deep_merge'], %w[simple complex_deep_merge], - ['complex_wide_structure'], %w[simple complex_wide_structure], - ['complex_nested_objects'], - %w[simple complex_nested_objects], - %w[complex_deep_merge complex_nested_objects complex_wide_structure] + %w[simple complex_deep_merge complex_nested_objects complex_wide_structure] ] Benchmark.bm do |x| diff --git a/rust/optify/src/provider/provider_trait.rs b/rust/optify/src/provider/provider_trait.rs index 97c50c5a..a5e16f5d 100644 --- a/rust/optify/src/provider/provider_trait.rs +++ b/rust/optify/src/provider/provider_trait.rs @@ -37,7 +37,7 @@ pub trait OptionsRegistry { /// Gets all feature names and alias names. fn get_features_and_aliases(&self) -> Vec; - /// Gets all options for the specified feature names + /// Gets all options for the specified feature names. fn get_all_options( &self, feature_names: &[impl AsRef],