diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 42b7a058..63b84f65 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -485,6 +485,12 @@ jobs: run: pixi run dev-py - name: Run tests run: pixi run test-py + - name: Upload failed images + uses: actions/upload-artifact@v6 + if: failure() + with: + name: failed-images-python-${{ matrix.options[0] }} + path: vl-convert-python/tests/failed/ # Build and test aarch64 Linux wheel using manylinux container vl-convert-python-tests-linux-aarch64: diff --git a/Cargo.lock b/Cargo.lock index 5d68c3c7..c3213c5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,6 +548,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "tokio", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -9775,6 +9785,7 @@ dependencies = [ name = "vl-convert-rs" version = "2.0.0-rc1" dependencies = [ + "backon", "base64 0.22.1", "criterion", "deno_ast", diff --git a/Cargo.toml b/Cargo.toml index 340d45a9..43d0e2ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ codegen-units = 1 [workspace.dependencies] anyhow = "1.0" assert_cmd = "2.0" +backon = { version = "1", default-features = false, features = ["tokio-sleep", "std"] } clap = { version = "4.5", features = ["derive"] } # Deno dependencies - versions correspond to Deno v2.6.6 diff --git a/thirdparty_rust.yaml b/thirdparty_rust.yaml index 576dcb2d..4634f6fb 100644 --- a/thirdparty_rust.yaml +++ b/thirdparty_rust.yaml @@ -7445,6 +7445,214 @@ third_party_libraries: DEALINGS IN THE SOFTWARE. - license: Apache-2.0 text: " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n" +- package_name: backon + package_version: 1.6.0 + repository: https://github.com/Xuanwo/backon + license: Apache-2.0 + licenses: + - license: Apache-2.0 + text: |2- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Datafuse Labs + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. - package_name: base16ct package_version: 0.2.0 repository: https://github.com/RustCrypto/formats/tree/master/base16ct diff --git a/vl-convert-python/README.md b/vl-convert-python/README.md index 2dfc301d..d16047b7 100644 --- a/vl-convert-python/README.md +++ b/vl-convert-python/README.md @@ -91,6 +91,22 @@ vg_spec = vlc.vegalite_to_vega(chart.to_json(), vl_version="4.17") with open("altair_chart.vg.json", "wt") as f: json.dump(vg_spec, f) ``` + +## Configure Worker Parallelism +By default, `vl-convert-python` uses `1` converter worker. You can configure this globally: + +```python +import vl_convert as vlc + +vlc.get_num_workers() # 1 +vlc.set_num_workers(4) # enable parallel worker pool +vlc.warm_up_workers() # optional: pre-initialize workers before first conversion +``` + +This setting applies to subsequent conversions and enables parallel work across Python threads. +Calling `warm_up_workers()` is optional and only needed if you want to avoid first-request +worker startup latency. + # How it works This crate uses [PyO3](https://pyo3.rs/) to wrap the [`vl-convert-rs`](https://crates.io/crates/vl-convert-rs) Rust crate as a Python library. The `vl-convert-rs` crate is a self-contained Rust library for converting [Vega-Lite](https://vega.github.io/vega-lite/) visualization specifications into various formats. The conversions are performed using the Vega-Lite and Vega JavaScript libraries running in a v8 JavaScript runtime provided by the [`deno_runtime`](https://crates.io/crates/deno_runtime) crate. Font metrics and SVG-to-PNG conversions are provided by the [`resvg`](https://crates.io/crates/resvg) crate. diff --git a/vl-convert-python/src/lib.rs b/vl-convert-python/src/lib.rs index fdc16d2f..0e9bbd8d 100644 --- a/vl-convert-python/src/lib.rs +++ b/vl-convert-python/src/lib.rs @@ -7,8 +7,9 @@ use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict}; use pythonize::{depythonize, pythonize}; use std::borrow::Cow; +use std::future::Future; use std::str::FromStr; -use std::sync::Mutex; +use std::sync::{Arc, RwLock}; use vl_convert_rs::converter::{ FormatLocale, Renderer, TimeFormatLocale, ValueOrString, VgOpts, VlOpts, }; @@ -25,7 +26,8 @@ use vl_convert_rs::VlConverter as VlConverterRs; extern crate lazy_static; lazy_static! { - static ref VL_CONVERTER: Mutex = Mutex::new(VlConverterRs::new()); + static ref VL_CONVERTER: RwLock> = + RwLock::new(Arc::new(VlConverterRs::new())); static ref PYTHON_RUNTIME: tokio::runtime::Runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -33,6 +35,23 @@ lazy_static! { .unwrap(); } +fn converter_read_handle() -> Result, vl_convert_rs::anyhow::Error> { + VL_CONVERTER + .read() + .map_err(|e| vl_convert_rs::anyhow::anyhow!("Failed to acquire converter read lock: {e}")) + .map(|guard| guard.clone()) +} + +fn run_converter_future(make_future: F) -> Result +where + F: FnOnce(Arc) -> Fut + Send + 'static, + Fut: Future> + 'static, + R: Send + 'static, +{ + let converter = converter_read_handle()?; + Python::with_gil(|py| py.allow_threads(move || PYTHON_RUNTIME.block_on(make_future(converter)))) +} + /// Convert a Vega-Lite spec to a Vega spec using a particular /// version of the Vega-Lite JavaScript library. /// @@ -63,21 +82,19 @@ fn vegalite_to_vega( Default::default() }; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - let vega_spec = match PYTHON_RUNTIME.block_on(converter.vegalite_to_vega( - vl_spec, - VlOpts { - vl_version, - config, - theme, - show_warnings: show_warnings.unwrap_or(false), - allowed_base_urls: None, - format_locale: None, - time_format_locale: None, - }, - )) { + let vl_opts = VlOpts { + vl_version, + config, + theme, + show_warnings: show_warnings.unwrap_or(false), + allowed_base_urls: None, + format_locale: None, + time_format_locale: None, + }; + + let vega_spec = match run_converter_future(move |converter| async move { + converter.vegalite_to_vega(vl_spec, vl_opts).await + }) { Ok(vega_spec) => vega_spec, Err(err) => { return Err(PyValueError::new_err(format!( @@ -115,18 +132,15 @@ fn vega_to_svg( let format_locale = parse_option_format_locale(format_locale)?; let time_format_locale = parse_option_time_format_locale(time_format_locale)?; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - - let svg = match PYTHON_RUNTIME.block_on(converter.vega_to_svg( - vg_spec, - VgOpts { - allowed_base_urls, - format_locale, - time_format_locale, - }, - )) { + let vg_opts = VgOpts { + allowed_base_urls, + format_locale, + time_format_locale, + }; + + let svg = match run_converter_future(move |converter| async move { + converter.vega_to_svg(vg_spec, vg_opts).await + }) { Ok(vega_spec) => vega_spec, Err(err) => { return Err(PyValueError::new_err(format!( @@ -161,10 +175,6 @@ fn vega_to_scenegraph( let format_locale = parse_option_format_locale(format_locale)?; let time_format_locale = parse_option_time_format_locale(time_format_locale)?; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - let vg_opts = VgOpts { allowed_base_urls, format_locale, @@ -174,11 +184,12 @@ fn vega_to_scenegraph( match format { "dict" => { let vg_spec = parse_json_spec(vg_spec)?; - let sg = PYTHON_RUNTIME - .block_on(converter.vega_to_scenegraph(vg_spec, vg_opts)) - .map_err(|err| { - PyValueError::new_err(format!("Vega to Scenegraph conversion failed:\n{err}")) - })?; + let sg = run_converter_future(move |converter| async move { + converter.vega_to_scenegraph(vg_spec, vg_opts).await + }) + .map_err(|err| { + PyValueError::new_err(format!("Vega to Scenegraph conversion failed:\n{err}")) + })?; Python::with_gil(|py| -> PyResult { pythonize(py, &sg) .map_err(|err| PyValueError::new_err(err.to_string())) @@ -187,11 +198,12 @@ fn vega_to_scenegraph( } "msgpack" => { let vg_spec = parse_spec_to_value_or_string(vg_spec)?; - let sg_bytes = PYTHON_RUNTIME - .block_on(converter.vega_to_scenegraph_msgpack(vg_spec, vg_opts)) - .map_err(|err| { - PyValueError::new_err(format!("Vega to Scenegraph conversion failed:\n{err}")) - })?; + let sg_bytes = run_converter_future(move |converter| async move { + converter.vega_to_scenegraph_msgpack(vg_spec, vg_opts).await + }) + .map_err(|err| { + PyValueError::new_err(format!("Vega to Scenegraph conversion failed:\n{err}")) + })?; Ok(Python::with_gil(|py| -> PyObject { PyBytes::new(py, sg_bytes.as_slice()).into() })) @@ -243,22 +255,19 @@ fn vegalite_to_svg( Default::default() }; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - - let svg = match PYTHON_RUNTIME.block_on(converter.vegalite_to_svg( - vl_spec, - VlOpts { - vl_version, - config, - theme, - show_warnings: show_warnings.unwrap_or(false), - allowed_base_urls, - format_locale, - time_format_locale, - }, - )) { + let vl_opts = VlOpts { + vl_version, + config, + theme, + show_warnings: show_warnings.unwrap_or(false), + allowed_base_urls, + format_locale, + time_format_locale, + }; + + let svg = match run_converter_future(move |converter| async move { + converter.vegalite_to_svg(vl_spec, vl_opts).await + }) { Ok(vega_spec) => vega_spec, Err(err) => { return Err(PyValueError::new_err(format!( @@ -312,10 +321,6 @@ fn vegalite_to_scenegraph( Default::default() }; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - let vl_opts = VlOpts { vl_version, config, @@ -329,13 +334,12 @@ fn vegalite_to_scenegraph( match format { "dict" => { let vl_spec = parse_json_spec(vl_spec)?; - let sg = PYTHON_RUNTIME - .block_on(converter.vegalite_to_scenegraph(vl_spec, vl_opts)) - .map_err(|err| { - PyValueError::new_err(format!( - "Vega-Lite to Scenegraph conversion failed:\n{err}" - )) - })?; + let sg = run_converter_future(move |converter| async move { + converter.vegalite_to_scenegraph(vl_spec, vl_opts).await + }) + .map_err(|err| { + PyValueError::new_err(format!("Vega-Lite to Scenegraph conversion failed:\n{err}")) + })?; Python::with_gil(|py| -> PyResult { pythonize(py, &sg) .map_err(|err| PyValueError::new_err(err.to_string())) @@ -344,13 +348,14 @@ fn vegalite_to_scenegraph( } "msgpack" => { let vl_spec = parse_spec_to_value_or_string(vl_spec)?; - let sg_bytes = PYTHON_RUNTIME - .block_on(converter.vegalite_to_scenegraph_msgpack(vl_spec, vl_opts)) - .map_err(|err| { - PyValueError::new_err(format!( - "Vega-Lite to Scenegraph conversion failed:\n{err}" - )) - })?; + let sg_bytes = run_converter_future(move |converter| async move { + converter + .vegalite_to_scenegraph_msgpack(vl_spec, vl_opts) + .await + }) + .map_err(|err| { + PyValueError::new_err(format!("Vega-Lite to Scenegraph conversion failed:\n{err}")) + })?; Ok(Python::with_gil(|py| -> PyObject { PyBytes::new(py, sg_bytes.as_slice()).into() })) @@ -389,20 +394,15 @@ fn vega_to_png( let format_locale = parse_option_format_locale(format_locale)?; let time_format_locale = parse_option_time_format_locale(time_format_locale)?; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - - let png_data = match PYTHON_RUNTIME.block_on(converter.vega_to_png( - vg_spec, - VgOpts { - allowed_base_urls, - format_locale, - time_format_locale, - }, - scale, - ppi, - )) { + let vg_opts = VgOpts { + allowed_base_urls, + format_locale, + time_format_locale, + }; + + let png_data = match run_converter_future(move |converter| async move { + converter.vega_to_png(vg_spec, vg_opts, scale, ppi).await + }) { Ok(vega_spec) => vega_spec, Err(err) => { return Err(PyValueError::new_err(format!( @@ -461,24 +461,21 @@ fn vegalite_to_png( let format_locale = parse_option_format_locale(format_locale)?; let time_format_locale = parse_option_time_format_locale(time_format_locale)?; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - - let png_data = match PYTHON_RUNTIME.block_on(converter.vegalite_to_png( - vl_spec, - VlOpts { - vl_version, - config, - theme, - show_warnings: show_warnings.unwrap_or(false), - allowed_base_urls, - format_locale, - time_format_locale, - }, - scale, - ppi, - )) { + let vl_opts = VlOpts { + vl_version, + config, + theme, + show_warnings: show_warnings.unwrap_or(false), + allowed_base_urls, + format_locale, + time_format_locale, + }; + + let png_data = match run_converter_future(move |converter| async move { + converter + .vegalite_to_png(vl_spec, vl_opts, scale, ppi) + .await + }) { Ok(vega_spec) => vega_spec, Err(err) => { return Err(PyValueError::new_err(format!( @@ -521,20 +518,17 @@ fn vega_to_jpeg( let format_locale = parse_option_format_locale(format_locale)?; let time_format_locale = parse_option_time_format_locale(time_format_locale)?; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - - let jpeg_data = match PYTHON_RUNTIME.block_on(converter.vega_to_jpeg( - vg_spec, - VgOpts { - allowed_base_urls, - format_locale, - time_format_locale, - }, - scale, - quality, - )) { + let vg_opts = VgOpts { + allowed_base_urls, + format_locale, + time_format_locale, + }; + + let jpeg_data = match run_converter_future(move |converter| async move { + converter + .vega_to_jpeg(vg_spec, vg_opts, scale, quality) + .await + }) { Ok(vega_spec) => vega_spec, Err(err) => { return Err(PyValueError::new_err(format!( @@ -593,24 +587,21 @@ fn vegalite_to_jpeg( let format_locale = parse_option_format_locale(format_locale)?; let time_format_locale = parse_option_time_format_locale(time_format_locale)?; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - - let jpeg_data = match PYTHON_RUNTIME.block_on(converter.vegalite_to_jpeg( - vl_spec, - VlOpts { - vl_version, - config, - theme, - show_warnings: show_warnings.unwrap_or(false), - allowed_base_urls, - format_locale, - time_format_locale, - }, - scale, - quality, - )) { + let vl_opts = VlOpts { + vl_version, + config, + theme, + show_warnings: show_warnings.unwrap_or(false), + allowed_base_urls, + format_locale, + time_format_locale, + }; + + let jpeg_data = match run_converter_future(move |converter| async move { + converter + .vegalite_to_jpeg(vl_spec, vl_opts, scale, quality) + .await + }) { Ok(vega_spec) => vega_spec, Err(err) => { return Err(PyValueError::new_err(format!( @@ -650,18 +641,15 @@ fn vega_to_pdf( let format_locale = parse_option_format_locale(format_locale)?; let time_format_locale = parse_option_time_format_locale(time_format_locale)?; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - - let pdf_bytes = match PYTHON_RUNTIME.block_on(converter.vega_to_pdf( - vg_spec, - VgOpts { - allowed_base_urls, - format_locale, - time_format_locale, - }, - )) { + let vg_opts = VgOpts { + allowed_base_urls, + format_locale, + time_format_locale, + }; + + let pdf_bytes = match run_converter_future(move |converter| async move { + converter.vega_to_pdf(vg_spec, vg_opts).await + }) { Ok(vega_spec) => vega_spec, Err(err) => { return Err(PyValueError::new_err(format!( @@ -716,22 +704,19 @@ fn vegalite_to_pdf( let format_locale = parse_option_format_locale(format_locale)?; let time_format_locale = parse_option_time_format_locale(time_format_locale)?; - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - - let pdf_data = match PYTHON_RUNTIME.block_on(converter.vegalite_to_pdf( - vl_spec, - VlOpts { - vl_version, - config, - theme, - show_warnings: false, - allowed_base_urls, - format_locale, - time_format_locale, - }, - )) { + let vl_opts = VlOpts { + vl_version, + config, + theme, + show_warnings: false, + allowed_base_urls, + format_locale, + time_format_locale, + }; + + let pdf_data = match run_converter_future(move |converter| async move { + converter.vegalite_to_pdf(vl_spec, vl_opts).await + }) { Ok(vega_spec) => vega_spec, Err(err) => { return Err(PyValueError::new_err(format!( @@ -821,24 +806,22 @@ fn vegalite_to_html( let format_locale = parse_option_format_locale(format_locale)?; let time_format_locale = parse_option_time_format_locale(time_format_locale)?; let renderer = renderer.unwrap_or_else(|| "svg".to_string()); - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - - Ok(PYTHON_RUNTIME.block_on(converter.vegalite_to_html( - vl_spec, - VlOpts { - vl_version, - config, - theme, - show_warnings: false, - allowed_base_urls: None, - format_locale, - time_format_locale, - }, - bundle.unwrap_or(false), - Renderer::from_str(&renderer)?, - ))?) + let renderer = Renderer::from_str(&renderer)?; + let vl_opts = VlOpts { + vl_version, + config, + theme, + show_warnings: false, + allowed_base_urls: None, + format_locale, + time_format_locale, + }; + + Ok(run_converter_future(move |converter| async move { + converter + .vegalite_to_html(vl_spec, vl_opts, bundle.unwrap_or(false), renderer) + .await + })?) } /// Convert a Vega spec to a self-contained HTML document @@ -866,19 +849,17 @@ fn vega_to_html( let format_locale = parse_option_format_locale(format_locale)?; let time_format_locale = parse_option_time_format_locale(time_format_locale)?; let renderer = renderer.unwrap_or_else(|| "svg".to_string()); - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - Ok(PYTHON_RUNTIME.block_on(converter.vega_to_html( - vg_spec, - VgOpts { - allowed_base_urls: None, - format_locale, - time_format_locale, - }, - bundle.unwrap_or(false), - Renderer::from_str(&renderer)?, - ))?) + let renderer = Renderer::from_str(&renderer)?; + let vg_opts = VgOpts { + allowed_base_urls: None, + format_locale, + time_format_locale, + }; + Ok(run_converter_future(move |converter| async move { + converter + .vega_to_html(vg_spec, vg_opts, bundle.unwrap_or(false), renderer) + .await + })?) } /// Convert an SVG image string to PNG image data @@ -1063,6 +1044,49 @@ fn register_font_directory(font_dir: &str) -> PyResult<()> { Ok(()) } +/// Set the number of parallel converter workers for subsequent requests +#[pyfunction] +#[pyo3(signature = (num_workers))] +fn set_num_workers(num_workers: usize) -> PyResult<()> { + if num_workers < 1 { + return Err(PyValueError::new_err("num_workers must be >= 1")); + } + + let converter = VlConverterRs::with_num_workers(num_workers).map_err(|err| { + PyValueError::new_err(format!( + "Failed to set worker count to {num_workers}: {err}" + )) + })?; + + let mut guard = VL_CONVERTER.write().map_err(|e| { + PyValueError::new_err(format!("Failed to acquire converter write lock: {e}")) + })?; + *guard = Arc::new(converter); + Ok(()) +} + +/// Get the number of configured converter workers +#[pyfunction] +#[pyo3(signature = ())] +fn get_num_workers() -> PyResult { + let guard = VL_CONVERTER.read().map_err(|e| { + PyValueError::new_err(format!("Failed to acquire converter read lock: {e}")) + })?; + Ok(guard.num_workers()) +} + +/// Eagerly start converter workers for the current worker-count configuration +#[pyfunction] +#[pyo3(signature = ())] +fn warm_up_workers() -> PyResult<()> { + let converter = converter_read_handle() + .map_err(|err| PyValueError::new_err(format!("warm_up_workers request failed:\n{err}")))?; + + Python::with_gil(|py| py.allow_threads(move || converter.warm_up())) + .map_err(|err| PyValueError::new_err(format!("warm_up_workers request failed:\n{err}")))?; + Ok(()) +} + /// Get the named local timezone that Vega uses to perform timezone calculations /// /// Returns: @@ -1071,18 +1095,16 @@ fn register_font_directory(font_dir: &str) -> PyResult<()> { #[pyfunction] #[pyo3(signature = ())] fn get_local_tz() -> PyResult> { - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - let local_tz = match PYTHON_RUNTIME.block_on(converter.get_local_tz()) { - Ok(local_tz) => local_tz, - Err(err) => { - return Err(PyValueError::new_err(format!( - "get_local_tz request failed:\n{}", - err - ))) - } - }; + let local_tz = + match run_converter_future(|converter| async move { converter.get_local_tz().await }) { + Ok(local_tz) => local_tz, + Err(err) => { + return Err(PyValueError::new_err(format!( + "get_local_tz request failed:\n{}", + err + ))) + } + }; Ok(local_tz) } @@ -1093,10 +1115,8 @@ fn get_local_tz() -> PyResult> { #[pyfunction] #[pyo3(signature = ())] fn get_themes() -> PyResult { - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - let themes = match PYTHON_RUNTIME.block_on(converter.get_themes()) { + let themes = match run_converter_future(|converter| async move { converter.get_themes().await }) + { Ok(themes) => themes, Err(err) => { return Err(PyValueError::new_err(format!( @@ -1196,13 +1216,14 @@ fn javascript_bundle(snippet: Option, vl_version: Option<&str>) -> PyRes Default::default() }; - if let Some(snippet) = &snippet { - Ok(PYTHON_RUNTIME.block_on(bundle_vega_snippet(snippet, vl_version))?) + if let Some(snippet) = snippet { + Ok(Python::with_gil(|py| { + py.allow_threads(|| PYTHON_RUNTIME.block_on(bundle_vega_snippet(&snippet, vl_version))) + })?) } else { - let mut converter = VL_CONVERTER - .lock() - .expect("Failed to acquire lock on Vega-Lite converter"); - Ok(PYTHON_RUNTIME.block_on(converter.get_vegaembed_bundle(vl_version))?) + Ok(run_converter_future(move |converter| async move { + converter.get_vegaembed_bundle(vl_version).await + })?) } } @@ -1271,6 +1292,9 @@ fn vl_convert(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(svg_to_jpeg, m)?)?; m.add_function(wrap_pyfunction!(svg_to_pdf, m)?)?; m.add_function(wrap_pyfunction!(register_font_directory, m)?)?; + m.add_function(wrap_pyfunction!(set_num_workers, m)?)?; + m.add_function(wrap_pyfunction!(get_num_workers, m)?)?; + m.add_function(wrap_pyfunction!(warm_up_workers, m)?)?; m.add_function(wrap_pyfunction!(get_local_tz, m)?)?; m.add_function(wrap_pyfunction!(get_themes, m)?)?; m.add_function(wrap_pyfunction!(get_format_locale, m)?)?; diff --git a/vl-convert-python/tests/test_specs.py b/vl-convert-python/tests/test_specs.py index 9c6a78c2..97789fa5 100644 --- a/vl-convert-python/tests/test_specs.py +++ b/vl-convert-python/tests/test_specs.py @@ -18,6 +18,7 @@ specs_dir = root_dir / "vl-convert-rs" / "tests" / "vl-specs" fonts_dir = root_dir / "vl-convert-rs" / "tests" / "fonts" locale_dir = root_dir / "vl-convert-rs" / "tests" / "locale" +failed_dir = tests_dir / "failed" BACKGROUND_COLOR = "#abc" @@ -247,11 +248,11 @@ def test_png(name, scale, as_dict): # Convert to vega first vg_spec = vlc.vegalite_to_vega(vl_spec, vl_version=vl_version) png = vlc.vega_to_png(vg_spec, scale=scale) - check_png(png, expected_png) + check_png(png, expected_png, name=f"png_vega_{name}") # Convert directly to image png = vlc.vegalite_to_png(vl_spec, vl_version=vl_version, scale=scale) - check_png(png, expected_png) + check_png(png, expected_png, name=f"png_vegalite_{name}") @pytest.mark.parametrize( @@ -272,7 +273,7 @@ def test_png_theme_config(name, scale, theme): theme=theme, config=config, ) - check_png(png, expected_png) + check_png(png, expected_png, name=f"png_theme_{name}_{theme}") @pytest.mark.parametrize( @@ -334,12 +335,12 @@ def test_pdf(name, tol, as_dict): pdf = vlc.vega_to_pdf(vg_spec) png = pdf_to_png(pdf) # Lower tolerance because pdfium does its own text rendering, which won't be pixel identical to resvg - check_png(png, expected_png, tol=tol) + check_png(png, expected_png, tol=tol, name=f"pdf_vega_{name}") # Convert directly to image pdf = vlc.vegalite_to_pdf(vl_spec, vl_version=vl_version) png = pdf_to_png(pdf) - check_png(png, expected_png, tol=tol) + check_png(png, expected_png, tol=tol, name=f"pdf_vegalite_{name}") @pytest.mark.skipif(sys.platform.startswith("win"), reason="Font mismatch on windows") @@ -363,7 +364,7 @@ def test_locale(): ) expected_png = load_expected_png(name, vl_version) - check_png(png, expected_png) + check_png(png, expected_png, name=f"png_locale_dict_{name}") # Test locale by name png = vlc.vegalite_to_png( @@ -375,7 +376,7 @@ def test_locale(): ) expected_png = load_expected_png(name, vl_version) - check_png(png, expected_png) + check_png(png, expected_png, name=f"png_locale_name_{name}") def test_gh_78(): @@ -388,14 +389,18 @@ def test_gh_78(): png = vlc.vegalite_to_png(vl_spec, vl_version=vl_version) expected_png = load_expected_png(name, vl_version) - check_png(png, expected_png) + check_png(png, expected_png, name=f"png_gh78_{name}") -def check_png(png, expected_png, tol=0.994): +def check_png(png, expected_png, tol=0.994, name=None): png_img = imread(BytesIO(png)) expected_png_img = imread(BytesIO(expected_png)) similarity_value = ssim(png_img, expected_png_img, channel_axis=2) if similarity_value < tol: + if name is not None: + failed_dir.mkdir(parents=True, exist_ok=True) + (failed_dir / f"{name}.png").write_bytes(png) + (failed_dir / f"{name}.expected.png").write_bytes(expected_png) pytest.fail(f"png mismatch with similarity: {similarity_value}") diff --git a/vl-convert-python/tests/test_workers.py b/vl-convert-python/tests/test_workers.py new file mode 100644 index 00000000..5aee0bd6 --- /dev/null +++ b/vl-convert-python/tests/test_workers.py @@ -0,0 +1,78 @@ +import concurrent.futures + +import pytest +import vl_convert as vlc + + +SIMPLE_VL_SPEC = { + "data": {"values": [{"a": "A", "b": 1}, {"a": "B", "b": 2}]}, + "mark": "bar", + "encoding": { + "x": {"field": "a", "type": "nominal"}, + "y": {"field": "b", "type": "quantitative"}, + }, +} + + +@pytest.fixture(autouse=True) +def reset_worker_count(): + original = vlc.get_num_workers() + vlc.set_num_workers(1) + try: + yield + finally: + vlc.set_num_workers(original) + + +def test_get_num_workers_default_one(): + assert vlc.get_num_workers() == 1 + + +def test_set_num_workers_rejects_zero(): + with pytest.raises(ValueError): + vlc.set_num_workers(0) + + +def test_parallel_threadpool_conversions_with_configured_workers(): + vlc.set_num_workers(4) + + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + futures = [ + executor.submit(vlc.vegalite_to_svg, SIMPLE_VL_SPEC, "v5_16") + for _ in range(16) + ] + svg_results = [future.result(timeout=30) for future in futures] + + assert len(svg_results) == 16 + assert all(svg.lstrip().startswith(" None: """ ... +def set_num_workers(num_workers: int) -> None: + """ + Set the number of converter workers for subsequent conversions. + + Parameters + ---------- + num_workers + Worker count (must be >= 1). + """ + ... + +def get_num_workers() -> int: + """ + Get the configured converter worker count. + + Returns + ------- + Number of workers. + """ + ... + +def warm_up_workers() -> None: + """ + Eagerly start converter workers for the current worker-count configuration. + + This can be used to avoid first-conversion startup latency by pre-initializing + worker runtimes before submitting conversion requests. + """ + ... + def svg_to_jpeg( svg: str, scale: float | None = None, quality: int | None = None ) -> bytes: diff --git a/vl-convert-rs/Cargo.toml b/vl-convert-rs/Cargo.toml index 486bdd2b..d3b18f49 100644 --- a/vl-convert-rs/Cargo.toml +++ b/vl-convert-rs/Cargo.toml @@ -41,6 +41,7 @@ tokio = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } lazy_static = { workspace = true } +backon = { workspace = true } rmp-serde = "1.3.0" base64 = "0.22.1" escape8259 = "0.5.3" diff --git a/vl-convert-rs/src/converter.rs b/vl-convert-rs/src/converter.rs index c4c5a22b..5a02399f 100644 --- a/vl-convert-rs/src/converter.rs +++ b/vl-convert-rs/src/converter.rs @@ -5,18 +5,20 @@ use crate::module_loader::{VlConvertModuleLoader, FORMATE_LOCALE_MAP, TIME_FORMA use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_core::op2; -use deno_core::{serde_v8, v8, ModuleSpecifier}; +use deno_core::{serde_v8, v8, ModuleSpecifier, OpState}; use deno_error::JsErrorBox; use deno_runtime::deno_fs::RealFs; use deno_runtime::deno_permissions::{PermissionsContainer, RuntimePermissionDescriptorParser}; use deno_runtime::deno_web::{BlobStore, InMemoryBroadcastChannel}; use deno_runtime::worker::{MainWorker, WorkerOptions, WorkerServiceOptions}; use deno_runtime::FeatureChecker; +use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Display, Formatter}; use std::io::Cursor; use std::rc::Rc; +use std::sync::Once; use std::sync::{Arc, Mutex}; use std::panic; @@ -25,8 +27,7 @@ use std::thread; use std::thread::JoinHandle; use deno_core::anyhow::anyhow; -use futures::channel::{mpsc, mpsc::Sender, oneshot}; -use futures_util::{SinkExt, StreamExt}; +use futures::channel::oneshot; use png::{PixelDimensions, Unit}; use svg2pdf::{ConversionOptions, PageOptions}; use tiny_skia::{Pixmap, PremultipliedColorU8}; @@ -37,7 +38,7 @@ use image::ImageReader; use resvg::render; use crate::text::{FONT_CONFIG, FONT_CONFIG_VERSION, USVG_OPTIONS}; -use std::sync::atomic::Ordering; +use std::sync::atomic::{AtomicUsize, Ordering}; // Extension with our custom ops - MainWorker provides all Web APIs (URL, fetch, etc.) // Canvas 2D ops are now in the separate vl_convert_canvas2d extension from vl-convert-canvas2d-deno @@ -57,152 +58,194 @@ deno_core::extension!( // Arguments are passed to V8 as JSON strings via Deno ops and parsed in JS. // Scenegraph results are returned as MessagePack byte buffers via ops, // avoiding JSON serialization overhead for large payloads. -struct VlConverterRuntime { - sender: Sender, - handle: JoinHandle>, +struct WorkerPool { + senders: Vec>, + // Per-worker count of requests that have been reserved for this worker but not yet + // fully processed. This includes in-flight senders blocked on channel capacity and + // commands currently queued/executing in the worker loop. + outstanding: Vec>, + dispatch_cursor: AtomicUsize, + _handles: Vec>, } -lazy_static! { - pub static ref TOKIO_RUNTIME: tokio::runtime::Runtime = - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap(); - static ref JSON_ARGS: Arc>> = Arc::new(Mutex::new(HashMap::new())); - static ref MSGPACK_RESULTS: Arc>>> = - Arc::new(Mutex::new(HashMap::new())); - static ref NEXT_ID: Arc> = Arc::new(Mutex::new(0)); -} - -static VL_CONVERTER_RUNTIME: Mutex> = Mutex::new(None); - -fn spawn_worker_thread() -> VlConverterRuntime { - let (sender, mut receiver) = mpsc::channel::(32); - let handle = thread::spawn(move || { - TOKIO_RUNTIME.block_on(async { - let mut inner = InnerVlConverter::try_new().await?; - while let Some(cmd) = receiver.next().await { - if let Err(e) = inner.refresh_font_config_if_needed() { - cmd.send_error(e); - continue; +impl WorkerPool { + fn next_sender(&self) -> Option<(tokio::sync::mpsc::Sender, OutstandingTicket)> { + if self.senders.is_empty() { + return None; + } + + // Choose the worker with the smallest outstanding count. Rotate scan start so ties + // are not biased to index 0. + let start = self.dispatch_cursor.fetch_add(1, Ordering::Relaxed) % self.senders.len(); + let mut best_idx = None; + let mut best_outstanding = usize::MAX; + + for offset in 0..self.senders.len() { + let idx = (start + offset) % self.senders.len(); + if self.senders[idx].is_closed() { + continue; + } + + let outstanding = self.outstanding[idx].load(Ordering::Relaxed); + if outstanding < best_outstanding { + best_idx = Some(idx); + best_outstanding = outstanding; + if outstanding == 0 { + break; } - match cmd { - VlConvertCommand::VlToVg { - vl_spec, - vl_opts, - responder, - } => { - let vega_spec = inner.vegalite_to_vega(vl_spec, vl_opts).await; - responder.send(vega_spec).ok(); - } - VlConvertCommand::VgToSvg { - vg_spec, - vg_opts, - responder, - } => { - let svg_result = inner.vega_to_svg(vg_spec, vg_opts).await; - responder.send(svg_result).ok(); - } - VlConvertCommand::VgToSg { - vg_spec, - vg_opts, - responder, - } => { - let sg_result = inner.vega_to_scenegraph(vg_spec, vg_opts).await; - responder.send(sg_result).ok(); - } - VlConvertCommand::VgToSgMsgpack { - vg_spec, - vg_opts, - responder, - } => { - let sg_result = inner.vega_to_scenegraph_msgpack(vg_spec, vg_opts).await; - responder.send(sg_result).ok(); - } - VlConvertCommand::VlToSvg { - vl_spec, - vl_opts, - responder, - } => { - let svg_result = inner.vegalite_to_svg(vl_spec, vl_opts).await; - responder.send(svg_result).ok(); - } - VlConvertCommand::VlToSg { - vl_spec, - vl_opts, - responder, - } => { - let sg_result = inner.vegalite_to_scenegraph(vl_spec, vl_opts).await; - responder.send(sg_result).ok(); - } - VlConvertCommand::VlToSgMsgpack { - vl_spec, - vl_opts, - responder, - } => { - let sg_result = - inner.vegalite_to_scenegraph_msgpack(vl_spec, vl_opts).await; - responder.send(sg_result).ok(); - } - VlConvertCommand::VgToPng { - vg_spec, - vg_opts, - scale, - ppi, - responder, - } => { - let png_result = match vg_spec.to_value() { - Ok(v) => inner.vega_to_png(&v, vg_opts, scale, ppi).await, - Err(e) => Err(e), - }; - responder.send(png_result).ok(); - } - VlConvertCommand::VlToPng { - vl_spec, - vl_opts, - scale, - ppi, - responder, - } => { - let png_result = match vl_spec.to_value() { - Ok(v) => inner.vegalite_to_png(&v, vl_opts, scale, ppi).await, - Err(e) => Err(e), - }; - responder.send(png_result).ok(); + } + } + + let idx = best_idx?; + let ticket = OutstandingTicket::new(self.outstanding[idx].clone()); + Some((self.senders[idx].clone(), ticket)) + } + + fn is_closed(&self) -> bool { + self.senders + .iter() + .all(tokio::sync::mpsc::Sender::is_closed) + } +} + +struct OutstandingTicket { + counter: Arc, +} + +impl OutstandingTicket { + fn new(counter: Arc) -> Self { + counter.fetch_add(1, Ordering::Relaxed); + Self { counter } + } +} + +impl Drop for OutstandingTicket { + fn drop(&mut self) { + let prev = self.counter.fetch_sub(1, Ordering::Relaxed); + debug_assert!(prev > 0, "outstanding counter underflow"); + } +} + +struct QueuedCommand { + cmd: VlConvertCommand, + ticket: OutstandingTicket, +} + +impl QueuedCommand { + fn new(cmd: VlConvertCommand, ticket: OutstandingTicket) -> Self { + Self { cmd, ticket } + } + + fn into_command(self) -> VlConvertCommand { + self.cmd + } + + fn into_parts(self) -> (VlConvertCommand, OutstandingTicket) { + (self.cmd, self.ticket) + } +} + +#[derive(Default)] +struct WorkerTransferState { + json_args: HashMap, + msgpack_results: HashMap>, + next_id: i32, +} + +type WorkerTransferStateHandle = Rc>; + +fn ensure_v8_platform_initialized() { + static V8_INIT: Once = Once::new(); + V8_INIT.call_once(|| deno_core::JsRuntime::init_platform(None, false)); +} + +fn worker_queue_capacity(num_workers: usize) -> usize { + num_workers.saturating_mul(32).max(32) +} + +fn spawn_worker_pool(num_workers: usize) -> Result { + if num_workers < 1 { + bail!("num_workers must be >= 1"); + } + ensure_v8_platform_initialized(); + + let total_queue_capacity = worker_queue_capacity(num_workers); + let per_worker_queue_capacity = (total_queue_capacity / num_workers).max(1); + let mut handles = Vec::with_capacity(num_workers); + let mut senders = Vec::with_capacity(num_workers); + let mut startup_receivers = Vec::with_capacity(num_workers); + + for _ in 0..num_workers { + let (tx, mut rx) = tokio::sync::mpsc::channel::(per_worker_queue_capacity); + senders.push(tx); + let (startup_tx, startup_rx) = std::sync::mpsc::channel::>(); + let handle = thread::spawn(move || { + let runtime = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(runtime) => runtime, + Err(err) => { + let _ = startup_tx.send(Err(format!("Failed to construct runtime: {err}"))); + return; + } + }; + let local = tokio::task::LocalSet::new(); + local.block_on(&runtime, async move { + let mut inner = match InnerVlConverter::try_new().await { + Ok(inner) => { + let _ = startup_tx.send(Ok(())); + inner } - VlConvertCommand::GetLocalTz { responder } => { - let local_tz = inner.get_local_tz().await; - responder.send(local_tz).ok(); + Err(err) => { + let _ = startup_tx.send(Err(err.to_string())); + return; } - VlConvertCommand::GetThemes { responder } => { - let themes = inner.get_themes().await; - responder.send(themes).ok(); + }; + + while let Some(queued_cmd) = rx.recv().await { + // Keep the ticket alive for the full loop iteration so outstanding + // covers refresh + command execution (drop happens at iteration end). + let (cmd, _ticket) = queued_cmd.into_parts(); + if let Err(e) = inner.refresh_font_config_if_needed() { + cmd.send_error(e); + continue; } + inner.handle_command(cmd).await; } + }); + }); + handles.push(handle); + startup_receivers.push(startup_rx); + } + + for startup_rx in startup_receivers { + match startup_rx.recv() { + Ok(Ok(())) => {} + Ok(Err(err)) => { + drop(senders); + for handle in handles { + let _ = handle.join(); + } + bail!("Failed to initialize worker: {err}"); + } + Err(err) => { + drop(senders); + for handle in handles { + let _ = handle.join(); + } + bail!("Failed to receive worker startup status: {err}"); } - Ok::<(), AnyError>(()) - })?; - Ok(()) - }); - VlConverterRuntime { sender, handle } -} - -/// Get a sender to the worker thread, respawning it if it has exited. -fn get_or_spawn_sender() -> Result, AnyError> { - let mut guard = VL_CONVERTER_RUNTIME - .lock() - .map_err(|e| anyhow!("Failed to lock worker runtime: {}", e))?; - - if let Some(ref runtime) = *guard { - if !runtime.handle.is_finished() { - return Ok(runtime.sender.clone()); } } - let runtime = spawn_worker_thread(); - let sender = runtime.sender.clone(); - *guard = Some(runtime); - Ok(sender) + let num = senders.len(); + Ok(WorkerPool { + senders, + outstanding: (0..num).map(|_| Arc::new(AtomicUsize::new(0))).collect(), + dispatch_cursor: AtomicUsize::new(0), + _handles: handles, + }) } /// A JSON value that may already be serialized to a string. @@ -390,93 +433,184 @@ impl VlOpts { } } -fn next_id() -> Result { - match NEXT_ID.lock() { - Ok(mut guard) => { - let id = *guard; - *guard = (*guard + 1) % i32::MAX; - Ok(id) - } - Err(err) => { - bail!("Failed to acquire lock: {err}") - } +fn next_id(transfer_state: &WorkerTransferStateHandle) -> Result { + let mut guard = transfer_state + .try_borrow_mut() + .map_err(|err| anyhow!("Failed to borrow worker transfer state: {err}"))?; + let id = guard.next_id; + guard.next_id = (guard.next_id + 1) % i32::MAX; + Ok(id) +} + +fn set_json_arg( + transfer_state: &WorkerTransferStateHandle, + arg: serde_json::Value, +) -> Result { + set_json_str_arg(transfer_state, serde_json::to_string(&arg)?) +} + +fn set_json_str_arg( + transfer_state: &WorkerTransferStateHandle, + json_str: String, +) -> Result { + let id = next_id(transfer_state)?; + let mut guard = transfer_state + .try_borrow_mut() + .map_err(|err| anyhow!("Failed to borrow worker transfer state: {err}"))?; + guard.json_args.insert(id, json_str); + Ok(id) +} + +fn set_spec_arg( + transfer_state: &WorkerTransferStateHandle, + spec: ValueOrString, +) -> Result { + match spec { + ValueOrString::JsonString(s) => set_json_str_arg(transfer_state, s), + ValueOrString::Value(v) => set_json_arg(transfer_state, v), } } -fn set_json_arg(arg: serde_json::Value) -> Result { - set_json_str_arg(serde_json::to_string(&arg)?) +fn alloc_msgpack_result_id(transfer_state: &WorkerTransferStateHandle) -> Result { + next_id(transfer_state) } -fn set_json_str_arg(json_str: String) -> Result { - let id = next_id()?; +fn take_msgpack_result( + transfer_state: &WorkerTransferStateHandle, + result_id: i32, +) -> Result, AnyError> { + let mut guard = transfer_state + .try_borrow_mut() + .map_err(|err| anyhow!("Failed to borrow worker transfer state: {err}"))?; + guard + .msgpack_results + .remove(&result_id) + .ok_or_else(|| anyhow!("Result id not found")) +} - match JSON_ARGS.lock() { - Ok(mut guard) => { - guard.insert(id, json_str); - } - Err(err) => { - bail!("Failed to acquire lock: {err}") - } +fn clear_json_arg(transfer_state: &WorkerTransferStateHandle, arg_id: i32) { + if let Ok(mut guard) = transfer_state.try_borrow_mut() { + guard.json_args.remove(&arg_id); } +} - Ok(id) +fn clear_msgpack_result(transfer_state: &WorkerTransferStateHandle, result_id: i32) { + if let Ok(mut guard) = transfer_state.try_borrow_mut() { + guard.msgpack_results.remove(&result_id); + } } -fn set_spec_arg(spec: ValueOrString) -> Result { - match spec { - ValueOrString::JsonString(s) => set_json_str_arg(s), - ValueOrString::Value(v) => set_json_arg(v), +struct JsonArgGuard { + transfer_state: WorkerTransferStateHandle, + arg_id: Option, +} + +impl JsonArgGuard { + fn from_value( + transfer_state: &WorkerTransferStateHandle, + value: serde_json::Value, + ) -> Result { + Ok(Self { + transfer_state: transfer_state.clone(), + arg_id: Some(set_json_arg(transfer_state, value)?), + }) + } + + fn from_spec( + transfer_state: &WorkerTransferStateHandle, + spec: ValueOrString, + ) -> Result { + Ok(Self { + transfer_state: transfer_state.clone(), + arg_id: Some(set_spec_arg(transfer_state, spec)?), + }) + } + + fn id(&self) -> i32 { + self.arg_id.expect("JsonArgGuard id missing") } } -fn alloc_msgpack_result_id() -> Result { - next_id() +impl Drop for JsonArgGuard { + fn drop(&mut self) { + if let Some(arg_id) = self.arg_id.take() { + clear_json_arg(&self.transfer_state, arg_id); + } + } +} + +struct MsgpackResultGuard { + transfer_state: WorkerTransferStateHandle, + result_id: Option, +} + +impl MsgpackResultGuard { + fn new(transfer_state: &WorkerTransferStateHandle) -> Result { + Ok(Self { + transfer_state: transfer_state.clone(), + result_id: Some(alloc_msgpack_result_id(transfer_state)?), + }) + } + + fn id(&self) -> i32 { + self.result_id.expect("MsgpackResultGuard id missing") + } + + fn take_result(mut self) -> Result, AnyError> { + let result_id = self + .result_id + .take() + .expect("MsgpackResultGuard id missing"); + take_msgpack_result(&self.transfer_state, result_id) + } } -fn take_msgpack_result(result_id: i32) -> Result, AnyError> { - match MSGPACK_RESULTS.lock() { - Ok(mut guard) => guard - .remove(&result_id) - .ok_or_else(|| anyhow!("Result id not found")), - Err(err) => bail!("Failed to acquire lock: {err}"), +impl Drop for MsgpackResultGuard { + fn drop(&mut self) { + if let Some(result_id) = self.result_id.take() { + clear_msgpack_result(&self.transfer_state, result_id); + } } } #[op2] #[string] -fn op_get_json_arg(arg_id: i32) -> Result { - match JSON_ARGS.lock() { - Ok(mut guard) => { - if let Some(arg) = guard.remove(&arg_id) { - Ok(arg) - } else { - Err(JsErrorBox::generic("Arg id not found")) - } - } - Err(err) => Err(JsErrorBox::generic(format!( - "Failed to acquire lock: {}", - err - ))), +fn op_get_json_arg(state: &mut OpState, arg_id: i32) -> Result { + let transfer_state = state + .try_borrow::() + .cloned() + .ok_or_else(|| JsErrorBox::generic("Worker transfer state not found"))?; + let mut guard = transfer_state.try_borrow_mut().map_err(|err| { + JsErrorBox::generic(format!("Failed to borrow worker transfer state: {err}")) + })?; + if let Some(arg) = guard.json_args.remove(&arg_id) { + Ok(arg) + } else { + Err(JsErrorBox::generic("Arg id not found")) } } #[op2(fast)] -fn op_set_msgpack_result(result_id: i32, #[buffer] data: &[u8]) -> Result<(), JsErrorBox> { - match MSGPACK_RESULTS.lock() { - Ok(mut guard) => { - guard.insert(result_id, data.to_vec()); - Ok(()) - } - Err(err) => Err(JsErrorBox::generic(format!( - "Failed to acquire lock: {}", - err - ))), - } +fn op_set_msgpack_result( + state: &mut OpState, + result_id: i32, + #[buffer] data: &[u8], +) -> Result<(), JsErrorBox> { + let transfer_state = state + .try_borrow::() + .cloned() + .ok_or_else(|| JsErrorBox::generic("Worker transfer state not found"))?; + let mut guard = transfer_state.try_borrow_mut().map_err(|err| { + JsErrorBox::generic(format!("Failed to borrow worker transfer state: {err}")) + })?; + guard.msgpack_results.insert(result_id, data.to_vec()); + Ok(()) } /// Struct that interacts directly with the Deno JavaScript runtime. Not Sendable struct InnerVlConverter { worker: MainWorker, + transfer_state: WorkerTransferStateHandle, initialized_vl_versions: HashSet, vega_initialized: bool, font_config_version: u64, @@ -889,7 +1023,7 @@ function vegaLiteToCanvas_{ver_name}(vlSpec, config, theme, warnings, allowedBas extensions: vec![ // Canvas 2D extension from vl-convert-canvas2d-deno crate vl_convert_canvas2d_deno::vl_convert_canvas2d::init(), - // Our runtime extension (text width, JSON args) + // Our runtime extension (worker-local JSON/msgpack transfer ops) vl_convert_runtime::init(), ], startup_snapshot: Some(crate::VL_CONVERT_SNAPSHOT), @@ -898,6 +1032,12 @@ function vegaLiteToCanvas_{ver_name}(vlSpec, config, theme, warnings, allowedBas // Create the MainWorker with full Web API support let worker = MainWorker::bootstrap_from_options(&main_module, services, options); + let transfer_state = Rc::new(RefCell::new(WorkerTransferState::default())); + worker + .js_runtime + .op_state() + .borrow_mut() + .put(transfer_state.clone()); // Add shared font config to OpState so canvas contexts use the same fonts as SVG rendering. // We resolve the FontConfig into a fontdb once here; each canvas context then clones @@ -915,6 +1055,7 @@ function vegaLiteToCanvas_{ver_name}(vlSpec, config, theme, warnings, allowedBas let this = Self { worker, + transfer_state, initialized_vl_versions: Default::default(), vega_initialized: false, font_config_version: initial_font_version, @@ -980,8 +1121,8 @@ function vegaLiteToCanvas_{ver_name}(vlSpec, config, theme, warnings, allowedBas self.init_vl_version(&vl_opts.vl_version).await?; let config = vl_opts.config.clone().unwrap_or(serde_json::Value::Null); - let spec_arg_id = set_spec_arg(vl_spec.into())?; - let config_arg_id = set_json_arg(config)?; + let spec_arg = JsonArgGuard::from_spec(&self.transfer_state, vl_spec.into())?; + let config_arg = JsonArgGuard::from_value(&self.transfer_state, config)?; let theme_arg = match &vl_opts.theme { None => "null".to_string(), @@ -998,8 +1139,8 @@ compileVegaLite_{ver_name:?}( ) "#, ver_name = vl_opts.vl_version, - spec_arg_id = spec_arg_id, - config_arg_id = config_arg_id, + spec_arg_id = spec_arg.id(), + config_arg_id = config_arg.id(), theme_arg = theme_arg, show_warnings = vl_opts.show_warnings, ); @@ -1028,12 +1169,13 @@ compileVegaLite_{ver_name:?}( Some(fl) => fl.as_object()?, }; - let spec_arg_id = set_spec_arg(vl_spec.into())?; - let config_arg_id = set_json_arg(config)?; + let spec_arg = JsonArgGuard::from_spec(&self.transfer_state, vl_spec.into())?; + let config_arg = JsonArgGuard::from_value(&self.transfer_state, config)?; let allowed_base_urls = serde_json::to_string(&serde_json::Value::from(vl_opts.allowed_base_urls))?; - let format_locale_id = set_json_arg(format_locale)?; - let time_format_locale_id = set_json_arg(time_format_locale)?; + let format_locale_arg = JsonArgGuard::from_value(&self.transfer_state, format_locale)?; + let time_format_locale_arg = + JsonArgGuard::from_value(&self.transfer_state, time_format_locale)?; let theme_arg = match &vl_opts.theme { None => "null".to_string(), @@ -1062,6 +1204,10 @@ vegaLiteToSvg_{ver_name:?}( "#, ver_name = vl_opts.vl_version, show_warnings = vl_opts.show_warnings, + spec_arg_id = spec_arg.id(), + config_arg_id = config_arg.id(), + format_locale_id = format_locale_arg.id(), + time_format_locale_id = time_format_locale_arg.id(), ); self.worker.js_runtime.execute_script("ext:", code)?; self.worker @@ -1093,13 +1239,14 @@ vegaLiteToSvg_{ver_name:?}( Some(fl) => fl.as_object()?, }; - let spec_arg_id = set_spec_arg(vl_spec)?; - let config_arg_id = set_json_arg(config)?; + let spec_arg = JsonArgGuard::from_spec(&self.transfer_state, vl_spec)?; + let config_arg = JsonArgGuard::from_value(&self.transfer_state, config)?; let allowed_base_urls = serde_json::to_string(&serde_json::Value::from(vl_opts.allowed_base_urls))?; - let format_locale_id = set_json_arg(format_locale)?; - let time_format_locale_id = set_json_arg(time_format_locale)?; - let result_id = alloc_msgpack_result_id()?; + let format_locale_arg = JsonArgGuard::from_value(&self.transfer_state, format_locale)?; + let time_format_locale_arg = + JsonArgGuard::from_value(&self.transfer_state, time_format_locale)?; + let result = MsgpackResultGuard::new(&self.transfer_state)?; let theme_arg = match &vl_opts.theme { None => "null".to_string(), @@ -1127,7 +1274,11 @@ vegaLiteToScenegraph_{ver_name:?}( "#, ver_name = vl_opts.vl_version, show_warnings = vl_opts.show_warnings, - result_id = result_id, + spec_arg_id = spec_arg.id(), + config_arg_id = config_arg.id(), + format_locale_id = format_locale_arg.id(), + time_format_locale_id = time_format_locale_arg.id(), + result_id = result.id(), ); self.worker.js_runtime.execute_script("ext:", code)?; self.worker @@ -1135,7 +1286,7 @@ vegaLiteToScenegraph_{ver_name:?}( .run_event_loop(Default::default()) .await?; - take_msgpack_result(result_id) + result.take_result() } pub async fn vegalite_to_scenegraph( @@ -1170,9 +1321,10 @@ vegaLiteToScenegraph_{ver_name:?}( Some(fl) => fl.as_object()?, }; - let arg_id = set_spec_arg(vg_spec.into())?; - let format_locale_id = set_json_arg(format_locale)?; - let time_format_locale_id = set_json_arg(time_format_locale)?; + let spec_arg = JsonArgGuard::from_spec(&self.transfer_state, vg_spec.into())?; + let format_locale_arg = JsonArgGuard::from_value(&self.transfer_state, format_locale)?; + let time_format_locale_arg = + JsonArgGuard::from_value(&self.transfer_state, time_format_locale)?; let code = format!( r#" @@ -1191,6 +1343,9 @@ vegaToSvg( svg = result; }}) "#, + arg_id = spec_arg.id(), + format_locale_id = format_locale_arg.id(), + time_format_locale_id = time_format_locale_arg.id(), ); self.worker.js_runtime.execute_script("ext:", code)?; self.worker @@ -1221,10 +1376,11 @@ vegaToSvg( Some(fl) => fl.as_object()?, }; - let arg_id = set_spec_arg(vg_spec)?; - let format_locale_id = set_json_arg(format_locale)?; - let time_format_locale_id = set_json_arg(time_format_locale)?; - let result_id = alloc_msgpack_result_id()?; + let spec_arg = JsonArgGuard::from_spec(&self.transfer_state, vg_spec)?; + let format_locale_arg = JsonArgGuard::from_value(&self.transfer_state, format_locale)?; + let time_format_locale_arg = + JsonArgGuard::from_value(&self.transfer_state, time_format_locale)?; + let result = MsgpackResultGuard::new(&self.transfer_state)?; let code = format!( r#" @@ -1242,7 +1398,10 @@ vegaToScenegraph( op_set_msgpack_result({result_id}, msgpack.encode(result)); }}) "#, - result_id = result_id, + arg_id = spec_arg.id(), + format_locale_id = format_locale_arg.id(), + time_format_locale_id = time_format_locale_arg.id(), + result_id = result.id(), ); self.worker.js_runtime.execute_script("ext:", code)?; self.worker @@ -1250,7 +1409,7 @@ vegaToScenegraph( .run_event_loop(Default::default()) .await?; - take_msgpack_result(result_id) + result.take_result() } pub async fn vega_to_scenegraph( @@ -1339,9 +1498,10 @@ delete themes.default Some(fl) => fl.as_object()?, }; - let arg_id = set_json_arg(vg_spec.clone())?; - let format_locale_id = set_json_arg(format_locale)?; - let time_format_locale_id = set_json_arg(time_format_locale)?; + let spec_arg = JsonArgGuard::from_value(&self.transfer_state, vg_spec.clone())?; + let format_locale_arg = JsonArgGuard::from_value(&self.transfer_state, format_locale)?; + let time_format_locale_arg = + JsonArgGuard::from_value(&self.transfer_state, time_format_locale)?; let code = format!( r#" @@ -1360,7 +1520,10 @@ vegaToCanvas( }} canvasPngData = canvas._toPngWithPpi({ppi}); }}) -"# +"#, + arg_id = spec_arg.id(), + format_locale_id = format_locale_arg.id(), + time_format_locale_id = time_format_locale_arg.id(), ); self.worker.js_runtime.execute_script("ext:", code)?; self.worker @@ -1394,10 +1557,11 @@ vegaToCanvas( Some(fl) => fl.as_object()?, }; - let spec_arg_id = set_json_arg(vl_spec.clone())?; - let config_arg_id = set_json_arg(config)?; - let format_locale_id = set_json_arg(format_locale)?; - let time_format_locale_id = set_json_arg(time_format_locale)?; + let spec_arg = JsonArgGuard::from_value(&self.transfer_state, vl_spec.clone())?; + let config_arg = JsonArgGuard::from_value(&self.transfer_state, config)?; + let format_locale_arg = JsonArgGuard::from_value(&self.transfer_state, format_locale)?; + let time_format_locale_arg = + JsonArgGuard::from_value(&self.transfer_state, time_format_locale)?; let theme_arg = match &vl_opts.theme { None => "null".to_string(), @@ -1430,6 +1594,10 @@ vegaLiteToCanvas_{ver_name:?}( "#, ver_name = vl_opts.vl_version, show_warnings = vl_opts.show_warnings, + spec_arg_id = spec_arg.id(), + config_arg_id = config_arg.id(), + format_locale_id = format_locale_arg.id(), + time_format_locale_id = time_format_locale_arg.id(), ); self.worker.js_runtime.execute_script("ext:", code)?; self.worker @@ -1440,6 +1608,101 @@ vegaLiteToCanvas_{ver_name:?}( let png_data = self.execute_script_to_bytes("canvasPngData").await?; Ok(png_data) } + + async fn handle_command(&mut self, cmd: VlConvertCommand) { + match cmd { + VlConvertCommand::VlToVg { + vl_spec, + vl_opts, + responder, + } => { + let vega_spec = self.vegalite_to_vega(vl_spec, vl_opts).await; + responder.send(vega_spec).ok(); + } + VlConvertCommand::VgToSvg { + vg_spec, + vg_opts, + responder, + } => { + let svg_result = self.vega_to_svg(vg_spec, vg_opts).await; + responder.send(svg_result).ok(); + } + VlConvertCommand::VgToSg { + vg_spec, + vg_opts, + responder, + } => { + let sg_result = self.vega_to_scenegraph(vg_spec, vg_opts).await; + responder.send(sg_result).ok(); + } + VlConvertCommand::VgToSgMsgpack { + vg_spec, + vg_opts, + responder, + } => { + let sg_result = self.vega_to_scenegraph_msgpack(vg_spec, vg_opts).await; + responder.send(sg_result).ok(); + } + VlConvertCommand::VlToSvg { + vl_spec, + vl_opts, + responder, + } => { + let svg_result = self.vegalite_to_svg(vl_spec, vl_opts).await; + responder.send(svg_result).ok(); + } + VlConvertCommand::VlToSg { + vl_spec, + vl_opts, + responder, + } => { + let sg_result = self.vegalite_to_scenegraph(vl_spec, vl_opts).await; + responder.send(sg_result).ok(); + } + VlConvertCommand::VlToSgMsgpack { + vl_spec, + vl_opts, + responder, + } => { + let sg_result = self.vegalite_to_scenegraph_msgpack(vl_spec, vl_opts).await; + responder.send(sg_result).ok(); + } + VlConvertCommand::VgToPng { + vg_spec, + vg_opts, + scale, + ppi, + responder, + } => { + let png_result = match vg_spec.to_value() { + Ok(v) => self.vega_to_png(&v, vg_opts, scale, ppi).await, + Err(e) => Err(e), + }; + responder.send(png_result).ok(); + } + VlConvertCommand::VlToPng { + vl_spec, + vl_opts, + scale, + ppi, + responder, + } => { + let png_result = match vl_spec.to_value() { + Ok(v) => self.vegalite_to_png(&v, vl_opts, scale, ppi).await, + Err(e) => Err(e), + }; + responder.send(png_result).ok(); + } + VlConvertCommand::GetLocalTz { responder } => { + let local_tz = self.get_local_tz().await; + responder.send(local_tz).ok(); + } + VlConvertCommand::GetThemes { responder } => { + let themes = self.get_themes().await; + responder.send(themes).ok(); + } + } + } } pub enum VlConvertCommand { @@ -1547,7 +1810,7 @@ impl VlConvertCommand { /// /// ``` /// use vl_convert_rs::{VlConverter, VlVersion}; -/// let mut converter = VlConverter::new(); +/// let converter = VlConverter::new(); /// /// let vl_spec: serde_json::Value = serde_json::from_str(r#" /// { @@ -1575,13 +1838,27 @@ impl VlConvertCommand { /// /// println!("{}", vega_spec) /// ``` +struct VlConverterInner { + vegaembed_bundles: Mutex>, + pool: Mutex>, + num_workers: usize, +} + #[derive(Clone)] pub struct VlConverter { - _vegaembed_bundles: HashMap, + inner: Arc, } impl VlConverter { pub fn new() -> Self { + Self::with_num_workers(1).expect("default worker count must be valid") + } + + pub fn with_num_workers(num_workers: usize) -> Result { + if num_workers < 1 { + bail!("num_workers must be >= 1"); + } + // Initialize environment logger with filter to suppress noisy SWC tree-shaker spans // The swc_ecma_transforms_optimization module logs tracing spans at ERROR level // which are not actual errors - just instrumentation. @@ -1590,223 +1867,210 @@ impl VlConverter { .try_init() .ok(); - Self { - _vegaembed_bundles: Default::default(), - } + Ok(Self { + inner: Arc::new(VlConverterInner { + vegaembed_bundles: Default::default(), + pool: Default::default(), + num_workers, + }), + }) } - pub async fn vegalite_to_vega( - &mut self, - vl_spec: impl Into, - vl_opts: VlOpts, - ) -> Result { - let vl_spec = vl_spec.into(); - let (resp_tx, resp_rx) = oneshot::channel::>(); - let cmd = VlConvertCommand::VlToVg { - vl_spec, - vl_opts, - responder: resp_tx, - }; + pub fn num_workers(&self) -> usize { + self.inner.num_workers + } + + /// Eagerly start the worker pool for this converter instance. + /// + /// This is optional; if not called, the pool starts lazily on first request. + pub fn warm_up(&self) -> Result<(), AnyError> { + let _ = self.get_or_spawn_sender()?; + Ok(()) + } + + fn get_or_spawn_sender( + &self, + ) -> Result<(tokio::sync::mpsc::Sender, OutstandingTicket), AnyError> { + let mut guard = self + .inner + .pool + .lock() + .map_err(|e| anyhow!("Failed to lock worker pool: {e}"))?; - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good + if let Some(pool) = guard.as_ref() { + if !pool.is_closed() { + if let Some(sender) = pool.next_sender() { + return Ok(sender); + } } - Err(err) => { - bail!("Failed to send conversion request: {err}") + *guard = None; + } + + let pool = spawn_worker_pool(self.inner.num_workers)?; + let sender = pool + .next_sender() + .ok_or_else(|| anyhow!("Worker pool has no senders"))?; + *guard = Some(pool); + Ok(sender) + } + + async fn send_command_with_retry( + &self, + cmd: VlConvertCommand, + request_name: &str, + ) -> Result<(), AnyError> { + let (sender, ticket) = self.get_or_spawn_sender()?; + let queued = QueuedCommand::new(cmd, ticket); + match sender.send(queued).await { + Ok(()) => Ok(()), + Err(tokio::sync::mpsc::error::SendError(queued)) => { + let cmd = queued.into_command(); + let (sender, ticket) = self.get_or_spawn_sender()?; + sender + .send(QueuedCommand::new(cmd, ticket)) + .await + .map_err(|err| { + anyhow!("Failed to send {request_name} request after retry: {err}") + }) } } + } - // Wait for result + async fn request( + &self, + make_cmd: impl FnOnce(oneshot::Sender>) -> VlConvertCommand, + request_name: &str, + ) -> Result { + let (resp_tx, resp_rx) = oneshot::channel::>(); + self.send_command_with_retry(make_cmd(resp_tx), request_name) + .await?; match resp_rx.await { - Ok(vega_spec_result) => vega_spec_result, - Err(err) => bail!("Failed to retrieve conversion result: {err}"), + Ok(result) => result, + Err(err) => bail!("Failed to retrieve {request_name} result: {err}"), } } + pub async fn vegalite_to_vega( + &self, + vl_spec: impl Into, + vl_opts: VlOpts, + ) -> Result { + let vl_spec = vl_spec.into(); + self.request( + move |responder| VlConvertCommand::VlToVg { + vl_spec, + vl_opts, + responder, + }, + "Vega-Lite to Vega conversion", + ) + .await + } + pub async fn vega_to_svg( - &mut self, + &self, vg_spec: impl Into, vg_opts: VgOpts, ) -> Result { let vg_spec = vg_spec.into(); - let (resp_tx, resp_rx) = oneshot::channel::>(); - let cmd = VlConvertCommand::VgToSvg { - vg_spec, - vg_opts, - responder: resp_tx, - }; - - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good - } - Err(err) => { - bail!("Failed to send SVG conversion request: {err}") - } - } - - // Wait for result - match resp_rx.await { - Ok(svg_result) => svg_result, - Err(err) => bail!("Failed to retrieve conversion result: {err}"), - } + self.request( + move |responder| VlConvertCommand::VgToSvg { + vg_spec, + vg_opts, + responder, + }, + "Vega to SVG conversion", + ) + .await } pub async fn vega_to_scenegraph( - &mut self, + &self, vg_spec: impl Into, vg_opts: VgOpts, ) -> Result { let vg_spec = vg_spec.into(); - let (resp_tx, resp_rx) = oneshot::channel::>(); - let cmd = VlConvertCommand::VgToSg { - vg_spec, - vg_opts, - responder: resp_tx, - }; - - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good - } - Err(err) => { - bail!("Failed to send Scenegraph conversion request: {err}") - } - } - - // Wait for result - match resp_rx.await { - Ok(svg_result) => svg_result, - Err(err) => bail!("Failed to retrieve conversion result: {err}"), - } + self.request( + move |responder| VlConvertCommand::VgToSg { + vg_spec, + vg_opts, + responder, + }, + "Vega to Scenegraph conversion", + ) + .await } pub async fn vega_to_scenegraph_msgpack( - &mut self, + &self, vg_spec: impl Into, vg_opts: VgOpts, ) -> Result, AnyError> { let vg_spec = vg_spec.into(); - let (resp_tx, resp_rx) = oneshot::channel::, AnyError>>(); - let cmd = VlConvertCommand::VgToSgMsgpack { - vg_spec, - vg_opts, - responder: resp_tx, - }; - - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good - } - Err(err) => { - bail!("Failed to send Scenegraph conversion request: {err}") - } - } - - // Wait for result - match resp_rx.await { - Ok(sg_msgpack_result) => sg_msgpack_result, - Err(err) => bail!("Failed to retrieve conversion result: {err}"), - } + self.request( + move |responder| VlConvertCommand::VgToSgMsgpack { + vg_spec, + vg_opts, + responder, + }, + "Vega to Scenegraph conversion", + ) + .await } pub async fn vegalite_to_svg( - &mut self, + &self, vl_spec: impl Into, vl_opts: VlOpts, ) -> Result { let vl_spec = vl_spec.into(); - let (resp_tx, resp_rx) = oneshot::channel::>(); - let cmd = VlConvertCommand::VlToSvg { - vl_spec, - vl_opts, - responder: resp_tx, - }; - - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good - } - Err(err) => { - bail!("Failed to send SVG conversion request: {err}") - } - } - - // Wait for result - match resp_rx.await { - Ok(svg_result) => svg_result, - Err(err) => bail!("Failed to retrieve conversion result: {err}"), - } + self.request( + move |responder| VlConvertCommand::VlToSvg { + vl_spec, + vl_opts, + responder, + }, + "Vega-Lite to SVG conversion", + ) + .await } pub async fn vegalite_to_scenegraph( - &mut self, + &self, vl_spec: impl Into, vl_opts: VlOpts, ) -> Result { let vl_spec = vl_spec.into(); - let (resp_tx, resp_rx) = oneshot::channel::>(); - let cmd = VlConvertCommand::VlToSg { - vl_spec, - vl_opts, - responder: resp_tx, - }; - - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good - } - Err(err) => { - bail!("Failed to send Scenegraph conversion request: {err}") - } - } - - // Wait for result - match resp_rx.await { - Ok(sg_result) => sg_result, - Err(err) => bail!("Failed to retrieve conversion result: {err}"), - } + self.request( + move |responder| VlConvertCommand::VlToSg { + vl_spec, + vl_opts, + responder, + }, + "Vega-Lite to Scenegraph conversion", + ) + .await } pub async fn vegalite_to_scenegraph_msgpack( - &mut self, + &self, vl_spec: impl Into, vl_opts: VlOpts, ) -> Result, AnyError> { let vl_spec = vl_spec.into(); - let (resp_tx, resp_rx) = oneshot::channel::, AnyError>>(); - let cmd = VlConvertCommand::VlToSgMsgpack { - vl_spec, - vl_opts, - responder: resp_tx, - }; - - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good - } - Err(err) => { - bail!("Failed to send Scenegraph conversion request: {err}") - } - } - - // Wait for result - match resp_rx.await { - Ok(sg_result) => sg_result, - Err(err) => bail!("Failed to retrieve conversion result: {err}"), - } + self.request( + move |responder| VlConvertCommand::VlToSgMsgpack { + vl_spec, + vl_opts, + responder, + }, + "Vega-Lite to Scenegraph conversion", + ) + .await } pub async fn vega_to_png( - &mut self, + &self, vg_spec: impl Into, vg_opts: VgOpts, scale: Option, @@ -1814,37 +2078,24 @@ impl VlConverter { ) -> Result, AnyError> { let scale = scale.unwrap_or(1.0); let ppi = ppi.unwrap_or(72.0); - // Calculate effective scale: combine user scale with ppi adjustment let effective_scale = scale * ppi / 72.0; + let vg_spec = vg_spec.into(); - let (resp_tx, resp_rx) = oneshot::channel::, AnyError>>(); - let cmd = VlConvertCommand::VgToPng { - vg_spec: vg_spec.into(), - vg_opts, - scale: effective_scale, - ppi, - responder: resp_tx, - }; - - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good - } - Err(err) => { - bail!("Failed to send PNG conversion request: {err}") - } - } - - // Wait for result - match resp_rx.await { - Ok(png_result) => png_result, - Err(err) => bail!("Failed to retrieve PNG conversion result: {err}"), - } + self.request( + move |responder| VlConvertCommand::VgToPng { + vg_spec, + vg_opts, + scale: effective_scale, + ppi, + responder, + }, + "Vega to PNG conversion", + ) + .await } pub async fn vegalite_to_png( - &mut self, + &self, vl_spec: impl Into, vl_opts: VlOpts, scale: Option, @@ -1852,37 +2103,24 @@ impl VlConverter { ) -> Result, AnyError> { let scale = scale.unwrap_or(1.0); let ppi = ppi.unwrap_or(72.0); - // Calculate effective scale: combine user scale with ppi adjustment let effective_scale = scale * ppi / 72.0; + let vl_spec = vl_spec.into(); - let (resp_tx, resp_rx) = oneshot::channel::, AnyError>>(); - let cmd = VlConvertCommand::VlToPng { - vl_spec: vl_spec.into(), - vl_opts, - scale: effective_scale, - ppi, - responder: resp_tx, - }; - - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good - } - Err(err) => { - bail!("Failed to send PNG conversion request: {err}") - } - } - - // Wait for result - match resp_rx.await { - Ok(png_result) => png_result, - Err(err) => bail!("Failed to retrieve PNG conversion result: {err}"), - } + self.request( + move |responder| VlConvertCommand::VlToPng { + vl_spec, + vl_opts, + scale: effective_scale, + ppi, + responder, + }, + "Vega-Lite to PNG conversion", + ) + .await } pub async fn vega_to_jpeg( - &mut self, + &self, vg_spec: impl Into, vg_opts: VgOpts, scale: Option, @@ -1894,7 +2132,7 @@ impl VlConverter { } pub async fn vegalite_to_jpeg( - &mut self, + &self, vl_spec: impl Into, vl_opts: VlOpts, scale: Option, @@ -1906,7 +2144,7 @@ impl VlConverter { } pub async fn vega_to_pdf( - &mut self, + &self, vg_spec: impl Into, vg_opts: VgOpts, ) -> Result, AnyError> { @@ -1915,7 +2153,7 @@ impl VlConverter { } pub async fn vegalite_to_pdf( - &mut self, + &self, vl_spec: impl Into, vl_opts: VlOpts, ) -> Result, AnyError> { @@ -1923,28 +2161,41 @@ impl VlConverter { svg_to_pdf(&svg) } - pub async fn get_vegaembed_bundle( - &mut self, - vl_version: VlVersion, - ) -> Result { - let bundle = match self._vegaembed_bundles.entry(vl_version) { + pub async fn get_vegaembed_bundle(&self, vl_version: VlVersion) -> Result { + if let Some(bundle) = self + .inner + .vegaembed_bundles + .lock() + .map_err(|e| anyhow!("Failed to lock bundle cache: {e}"))? + .get(&vl_version) + .cloned() + { + return Ok(bundle); + } + + let computed_bundle = bundle_vega_snippet( + "window.vegaEmbed=vegaEmbed; window.vega=vega; window.vegaLite=vegaLite;", + vl_version, + ) + .await?; + + let mut guard = self + .inner + .vegaembed_bundles + .lock() + .map_err(|e| anyhow!("Failed to lock bundle cache: {e}"))?; + let bundle = match guard.entry(vl_version) { Entry::Occupied(occupied) => occupied.get().clone(), Entry::Vacant(vacant) => { - let bundle = bundle_vega_snippet( - "window.vegaEmbed=vegaEmbed; window.vega=vega; window.vegaLite=vegaLite;", - vl_version, - ) - .await?; - vacant.insert(bundle.clone()); - bundle + vacant.insert(computed_bundle.clone()); + computed_bundle } }; - Ok(bundle) } async fn build_html( - &mut self, + &self, code: &str, vl_version: VlVersion, bundle: bool, @@ -1997,7 +2248,7 @@ impl VlConverter { } pub async fn vegalite_to_html( - &mut self, + &self, vl_spec: impl Into, vl_opts: VlOpts, bundle: bool, @@ -2009,7 +2260,7 @@ impl VlConverter { } pub async fn vega_to_html( - &mut self, + &self, vg_spec: impl Into, vg_opts: VgOpts, bundle: bool, @@ -2019,46 +2270,20 @@ impl VlConverter { self.build_html(&code, Default::default(), bundle).await } - pub async fn get_local_tz(&mut self) -> Result, AnyError> { - let (resp_tx, resp_rx) = oneshot::channel::, AnyError>>(); - let cmd = VlConvertCommand::GetLocalTz { responder: resp_tx }; - - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good - } - Err(err) => { - bail!("Failed to send get_local_tz request: {err}") - } - } - - // Wait for result - match resp_rx.await { - Ok(local_tz_result) => local_tz_result, - Err(err) => bail!("Failed to retrieve get_local_tz result: {err}"), - } + pub async fn get_local_tz(&self) -> Result, AnyError> { + self.request( + |responder| VlConvertCommand::GetLocalTz { responder }, + "get_local_tz", + ) + .await } - pub async fn get_themes(&mut self) -> Result { - let (resp_tx, resp_rx) = oneshot::channel::>(); - let cmd = VlConvertCommand::GetThemes { responder: resp_tx }; - - // Send request - match get_or_spawn_sender()?.send(cmd).await { - Ok(_) => { - // All good - } - Err(err) => { - bail!("Failed to send get_themes request: {err}") - } - } - - // Wait for result - match resp_rx.await { - Ok(themes_result) => themes_result, - Err(err) => bail!("Failed to retrieve get_themes result: {err}"), - } + pub async fn get_themes(&self) -> Result { + self.request( + |responder| VlConvertCommand::GetThemes { responder }, + "get_themes", + ) + .await } } @@ -2245,9 +2470,15 @@ mod tests { use super::*; use serde_json::json; + fn make_test_command() -> VlConvertCommand { + let (responder, _rx) = + futures::channel::oneshot::channel::, AnyError>>(); + VlConvertCommand::GetLocalTz { responder } + } + #[tokio::test] async fn test_convert_context() { - let mut ctx = VlConverter::new(); + let ctx = VlConverter::new(); let vl_spec: serde_json::Value = serde_json::from_str(r#" { "data": {"url": "https://raw.githubusercontent.com/vega/vega-datasets/master/data/seattle-weather.csv"}, @@ -2285,7 +2516,7 @@ mod tests { } "#).unwrap(); - let mut ctx1 = VlConverter::new(); + let ctx1 = VlConverter::new(); let vg_spec1 = ctx1 .vegalite_to_vega( vl_spec.clone(), @@ -2298,7 +2529,7 @@ mod tests { .unwrap(); println!("vg_spec1: {}", vg_spec1); - let mut ctx1 = VlConverter::new(); + let ctx1 = VlConverter::new(); let vg_spec2 = ctx1 .vegalite_to_vega( vl_spec, @@ -2569,6 +2800,275 @@ try { assert_eq!(url, expected); } + #[test] + fn test_with_num_workers_rejects_zero() { + let err = VlConverter::with_num_workers(0).err().unwrap(); + assert!(err.to_string().contains("num_workers must be >= 1")); + } + + #[test] + fn test_num_workers_reports_configured_value() { + let converter = VlConverter::with_num_workers(4).unwrap(); + assert_eq!(converter.num_workers(), 4); + } + + #[test] + fn test_worker_pool_next_sender_balances_outstanding_reservations() { + let mut senders = Vec::new(); + let mut _receivers = Vec::new(); + for _ in 0..3 { + let (tx, rx) = tokio::sync::mpsc::channel::(1); + senders.push(tx); + _receivers.push(rx); + } + + let pool = WorkerPool { + senders, + outstanding: (0..3) + .map(|_| std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0))) + .collect(), + dispatch_cursor: std::sync::atomic::AtomicUsize::new(0), + _handles: Vec::new(), + }; + + let mut tickets = Vec::new(); + for _ in 0..30 { + let (_, ticket) = pool + .next_sender() + .expect("pool with open senders should produce a sender"); + tickets.push(ticket); + + let loads: Vec = pool + .outstanding + .iter() + .map(|outstanding| outstanding.load(std::sync::atomic::Ordering::Relaxed)) + .collect(); + let min = *loads.iter().min().expect("loads should not be empty"); + let max = *loads.iter().max().expect("loads should not be empty"); + assert!( + max - min <= 1, + "expected balanced outstanding counts, got {loads:?}" + ); + } + + drop(tickets); + for outstanding in &pool.outstanding { + assert_eq!(outstanding.load(std::sync::atomic::Ordering::Relaxed), 0); + } + } + + #[test] + fn test_worker_pool_next_sender_skips_closed_senders() { + let (closed_sender, closed_receiver) = tokio::sync::mpsc::channel::(1); + drop(closed_receiver); + + let (open_sender, mut open_receiver) = tokio::sync::mpsc::channel::(1); + + let pool = WorkerPool { + senders: vec![closed_sender, open_sender], + outstanding: (0..2) + .map(|_| std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0))) + .collect(), + dispatch_cursor: std::sync::atomic::AtomicUsize::new(0), + _handles: Vec::new(), + }; + + for _ in 0..4 { + let (sender, ticket) = pool + .next_sender() + .expect("pool should return the open sender"); + sender + .try_send(QueuedCommand::new(make_test_command(), ticket)) + .expect("dispatch should use open sender, not closed sender"); + let queued = open_receiver + .try_recv() + .expect("open receiver should receive dispatched command"); + drop(queued); + } + } + + #[tokio::test] + async fn test_worker_pool_cancellation_releases_outstanding_ticket() { + let (sender, mut receiver) = tokio::sync::mpsc::channel::(1); + let pool = WorkerPool { + senders: vec![sender], + outstanding: vec![std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0))], + dispatch_cursor: std::sync::atomic::AtomicUsize::new(0), + _handles: Vec::new(), + }; + + let (sender, ticket) = pool.next_sender().unwrap(); + sender + .send(QueuedCommand::new(make_test_command(), ticket)) + .await + .unwrap(); + assert_eq!( + pool.outstanding[0].load(std::sync::atomic::Ordering::Relaxed), + 1 + ); + + let (sender, ticket) = pool.next_sender().unwrap(); + let blocked_send = tokio::spawn(async move { + sender + .send(QueuedCommand::new(make_test_command(), ticket)) + .await + }); + tokio::task::yield_now().await; + + assert_eq!( + pool.outstanding[0].load(std::sync::atomic::Ordering::Relaxed), + 2 + ); + + blocked_send.abort(); + let _ = blocked_send.await; + tokio::task::yield_now().await; + + assert_eq!( + pool.outstanding[0].load(std::sync::atomic::Ordering::Relaxed), + 1 + ); + + let queued = receiver + .recv() + .await + .expect("first queued command should still be in the channel"); + drop(queued); + + assert_eq!( + pool.outstanding[0].load(std::sync::atomic::Ordering::Relaxed), + 0 + ); + } + + #[test] + fn test_warm_up_respawns_closed_pool_without_explicit_reset() { + let num_workers = 2; + let converter = VlConverter::with_num_workers(num_workers).unwrap(); + + let mut closed_senders = Vec::with_capacity(num_workers); + for _ in 0..num_workers { + let (sender, receiver) = tokio::sync::mpsc::channel::(1); + drop(receiver); + closed_senders.push(sender); + } + + let closed_pool = WorkerPool { + senders: closed_senders, + outstanding: (0..num_workers) + .map(|_| std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0))) + .collect(), + dispatch_cursor: std::sync::atomic::AtomicUsize::new(0), + _handles: Vec::new(), + }; + + { + let mut guard = converter.inner.pool.lock().unwrap(); + *guard = Some(closed_pool); + } + + converter.warm_up().unwrap(); + + let guard = converter.inner.pool.lock().unwrap(); + let pool = guard + .as_ref() + .expect("warm_up should replace closed pool with a live pool"); + assert_eq!(pool.senders.len(), num_workers); + assert!(!pool.is_closed(), "respawned pool should be open"); + } + + #[test] + fn test_warm_up_spawns_pool_without_request() { + let converter = VlConverter::with_num_workers(2).unwrap(); + + { + let guard = converter.inner.pool.lock().unwrap(); + assert!(guard.is_none(), "pool should start uninitialized"); + } + + converter.warm_up().unwrap(); + + { + let guard = converter.inner.pool.lock().unwrap(); + let pool = guard + .as_ref() + .expect("pool should be initialized by warm_up"); + assert_eq!(pool.senders.len(), 2); + assert!(!pool.is_closed(), "warmed pool should have open senders"); + assert_eq!( + pool.outstanding + .iter() + .map(|outstanding| outstanding.load(std::sync::atomic::Ordering::Relaxed)) + .sum::(), + 0, + "warm_up should not leave outstanding reservations" + ); + } + } + + #[tokio::test] + async fn test_warm_up_is_idempotent() { + let converter = VlConverter::with_num_workers(2).unwrap(); + converter.warm_up().unwrap(); + converter.warm_up().unwrap(); + + let vl_spec = serde_json::json!({ + "data": {"values": [{"a": "A", "b": 1}, {"a": "B", "b": 2}]}, + "mark": "bar", + "encoding": { + "x": {"field": "a", "type": "nominal"}, + "y": {"field": "b", "type": "quantitative"} + } + }); + + let svg = converter + .vegalite_to_svg( + vl_spec, + VlOpts { + vl_version: VlVersion::v5_16, + ..Default::default() + }, + ) + .await + .unwrap(); + assert!(svg.trim_start().starts_with(" usvg::ImageHrefStringResolverFn<'static> { Box::new(move |href: &str, opts: &Options| { info!("Resolving image: {href}"); if href.starts_with("http://") || href.starts_with("https://") { - // Download image to temporary file with reqwest + // Download image to temporary file with reqwest, retrying on transient errors let (bytes, content_type): (Option<_>, Option<_>) = task::block_in_place(move || { IMAGE_TOKIO_RUNTIME.block_on(async { - if let Ok(response) = REQWEST_CLIENT.get(href).send().await { - let content_type = response.headers().get("Content-Type") + let result = (|| async { + let response = REQWEST_CLIENT.get(href).send().await?; + let status = response.status(); + let content_type = response + .headers() + .get("Content-Type") .and_then(|h| h.to_str().ok().map(|c| c.to_string())); - // Check status code. - match response.status() { - StatusCode::OK => (response.bytes().await.ok(), content_type), - status => { - let msg = response + match status { + StatusCode::OK => { + let bytes = response.bytes().await?; + Ok((Some(bytes), content_type)) + } + s if s.is_server_error() || s == StatusCode::TOO_MANY_REQUESTS => { + // Transient HTTP error — signal for retry + Err(response.error_for_status().unwrap_err()) + } + s => { + // Permanent HTTP error — log and short-circuit without retrying + let body = response .bytes() .await - .map(|b| String::from_utf8_lossy(b.as_ref()).to_string()); - if let Ok(msg) = msg { - error!( - "Failed to load image from url {} with status code {:?}\n{}", - href, status, msg - ); - } else { - error!( - "Failed to load image from url {} with status code {:?}", - href, status - ); - } - (None, None) + .map(|b| String::from_utf8_lossy(&b).to_string()) + .unwrap_or_default(); + error!( + "Failed to load image from url {} with status code {:?}\n{}", + href, s, body + ); + Ok((None, None)) } } - } else { - (None, None) + }) + .retry( + ExponentialBuilder::default() + .with_min_delay(Duration::from_millis(500)) + .with_max_delay(Duration::from_secs(10)) + .with_max_times(4), + ) + .when(|e| { + // Retry on network errors (no status) and transient HTTP errors + e.status() + .map(|s| s.is_server_error() || s == StatusCode::TOO_MANY_REQUESTS) + .unwrap_or(true) + }) + .notify(|err, dur| { + warn!( + "Retrying image load from {} in {:.1}s: {}", + href, + dur.as_secs_f32(), + err + ); + }) + .await; + + match result { + Ok((bytes, content_type)) => (bytes, content_type), + Err(e) => { + error!("Failed to load image from url {}: {}", href, e); + (None, None) + } } }) }); diff --git a/vl-convert-rs/tests/test_specs.rs b/vl-convert-rs/tests/test_specs.rs index fb57054e..49933ac5 100644 --- a/vl-convert-rs/tests/test_specs.rs +++ b/vl-convert-rs/tests/test_specs.rs @@ -451,7 +451,7 @@ mod test_vegalite_to_vega { let vl_spec = load_vl_spec(name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); let vg_result = block_on( converter.vegalite_to_vega(vl_spec, VlOpts{vl_version, ..Default::default()} @@ -496,7 +496,7 @@ mod test_vegalite_to_html_no_bundle { let vl_spec = load_vl_spec(name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); let html_result = block_on( converter.vegalite_to_html(vl_spec, VlOpts{vl_version, ..Default::default()}, false, Renderer::Canvas) @@ -544,7 +544,7 @@ mod test_vegalite_to_html_bundle { let vl_spec = load_vl_spec(name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); let html_result = block_on( converter.vegalite_to_html(vl_spec, VlOpts{vl_version, ..Default::default()}, true, Renderer::Svg) @@ -589,7 +589,7 @@ mod test_svg { let vl_spec = load_vl_spec(name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Convert to vega first let vg_spec = @@ -624,7 +624,7 @@ mod test_svg_allowed_base_url { let vl_spec = load_vl_spec(name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Convert to vega first let vg_spec = block_on(converter.vegalite_to_vega( @@ -717,7 +717,7 @@ mod test_scenegraph { let vl_spec = load_vl_spec(name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Convert to vega first let vg_spec = @@ -775,7 +775,7 @@ mod test_png_no_theme { let vl_spec = load_vl_spec(name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Convert to vega first let vg_spec = block_on( @@ -823,7 +823,7 @@ mod test_png_theme_config { let vl_spec = load_vl_spec(name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Convert directly to png with theme and config that overrides background color let png_data = block_on( @@ -884,7 +884,7 @@ async fn test_font_with_quotes() { let vl_spec = load_vl_spec(name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); let png_data = converter .vegalite_to_png( @@ -916,7 +916,7 @@ async fn test_locale() { load_locale(format_locale_name, time_format_locale_name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Convert with locale objects let png_data = converter @@ -984,7 +984,7 @@ mod test_jpeg { let vl_spec = load_vl_spec(name); // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Convert to vega first let vg_spec = @@ -1019,7 +1019,7 @@ mod test_vega_label_transform { initialize(); let vg_spec = load_vg_spec(name); - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); let png_data = block_on( converter.vega_to_png(vg_spec, Default::default(), Some(scale), None) diff --git a/vl-convert-rs/tests/test_themes.rs b/vl-convert-rs/tests/test_themes.rs index 326f47e1..45ea0be2 100644 --- a/vl-convert-rs/tests/test_themes.rs +++ b/vl-convert-rs/tests/test_themes.rs @@ -3,7 +3,7 @@ use vl_convert_rs::VlConverter; #[tokio::test] async fn test_get_themes_dark_background() { // Create Vega-Lite Converter and perform conversion - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); if let serde_json::Value::Object(all_themes) = converter.get_themes().await.unwrap() { if let serde_json::Value::Object(dark) = all_themes.get("dark").unwrap() { let background = dark.get("background").unwrap().as_str().unwrap(); diff --git a/vl-convert-rs/thirdparty_rust.yaml b/vl-convert-rs/thirdparty_rust.yaml index 576dcb2d..4634f6fb 100644 --- a/vl-convert-rs/thirdparty_rust.yaml +++ b/vl-convert-rs/thirdparty_rust.yaml @@ -7445,6 +7445,214 @@ third_party_libraries: DEALINGS IN THE SOFTWARE. - license: Apache-2.0 text: " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n" +- package_name: backon + package_version: 1.6.0 + repository: https://github.com/Xuanwo/backon + license: Apache-2.0 + licenses: + - license: Apache-2.0 + text: |2- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Datafuse Labs + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. - package_name: base16ct package_version: 0.2.0 repository: https://github.com/RustCrypto/formats/tree/master/base16ct diff --git a/vl-convert/src/main.rs b/vl-convert/src/main.rs index 8ca0c5fa..673687ea 100644 --- a/vl-convert/src/main.rs +++ b/vl-convert/src/main.rs @@ -714,7 +714,7 @@ async fn main() -> Result<(), anyhow::Error> { }; let renderer = renderer.unwrap_or_else(|| "svg".to_string()); - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); let html = converter .vegalite_to_html( vl_spec, @@ -847,7 +847,7 @@ async fn main() -> Result<(), anyhow::Error> { let renderer = renderer.unwrap_or_else(|| "svg".to_string()); - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); let html = converter .vega_to_html( vg_spec, @@ -1200,7 +1200,7 @@ async fn vl_2_vg( let config = read_config_json(config)?; // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Perform conversion let vega_json = match converter @@ -1265,7 +1265,7 @@ async fn vg_2_svg( }; // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Perform conversion let svg = match converter @@ -1317,7 +1317,7 @@ async fn vg_2_png( }; // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Perform conversion let png_data = match converter @@ -1371,7 +1371,7 @@ async fn vg_2_jpeg( }; // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Perform conversion let jpeg_data = match converter @@ -1423,7 +1423,7 @@ async fn vg_2_pdf( }; // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Perform conversion let pdf_data = match converter @@ -1484,7 +1484,7 @@ async fn vl_2_svg( }; // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Perform conversion let svg = match converter @@ -1551,7 +1551,7 @@ async fn vl_2_png( }; // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Perform conversion let png_data = match converter @@ -1620,7 +1620,7 @@ async fn vl_2_jpeg( }; // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Perform conversion let jpeg_data = match converter @@ -1687,7 +1687,7 @@ async fn vl_2_pdf( }; // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); // Perform conversion let pdf_data = match converter @@ -1719,7 +1719,7 @@ async fn vl_2_pdf( async fn list_themes() -> Result<(), anyhow::Error> { // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); if let serde_json::Value::Object(themes) = converter.get_themes().await? { for theme in themes.keys().sorted() { @@ -1734,7 +1734,7 @@ async fn list_themes() -> Result<(), anyhow::Error> { async fn cat_theme(theme: &str) -> Result<(), anyhow::Error> { // Initialize converter - let mut converter = VlConverter::new(); + let converter = VlConverter::new(); if let serde_json::Value::Object(themes) = converter.get_themes().await? { if let Some(theme_config) = themes.get(theme) { diff --git a/vl-convert/thirdparty_rust.yaml b/vl-convert/thirdparty_rust.yaml index 576dcb2d..4634f6fb 100644 --- a/vl-convert/thirdparty_rust.yaml +++ b/vl-convert/thirdparty_rust.yaml @@ -7445,6 +7445,214 @@ third_party_libraries: DEALINGS IN THE SOFTWARE. - license: Apache-2.0 text: " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\nCopyright [yyyy] [name of copyright owner]\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n" +- package_name: backon + package_version: 1.6.0 + repository: https://github.com/Xuanwo/backon + license: Apache-2.0 + licenses: + - license: Apache-2.0 + text: |2- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Datafuse Labs + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. - package_name: base16ct package_version: 0.2.0 repository: https://github.com/RustCrypto/formats/tree/master/base16ct