Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ideas on how to improve compile-time #987

Open
2 of 3 tasks
NobodyXu opened this issue Aug 19, 2023 · 26 comments
Open
2 of 3 tasks

Ideas on how to improve compile-time #987

NobodyXu opened this issue Aug 19, 2023 · 26 comments
Labels
acknowledged an issue is accepted as shortcoming to be fixed C-tracking-issue An issue to track to track the progress of multiple PRs or issues enhancement New feature or request help wanted Extra attention is needed

Comments

@NobodyXu
Copy link
Contributor

NobodyXu commented Aug 19, 2023

Summary 💡

De-monomorphisation

Function like gix::clone::PrepareFetch::new can be trivally de-monomorphised by extracting an inner function.

@Byron It's possible we can do this without sacrificing code readability by creating our own variant of momo.

For gix::clone::PrepareFetch::fetch_then_checkout it would be harder since Progress is not an dyn-safe trait.

I would like to add another provided method to Progress:

/// An opaque type so that `gix` can internally change its implementation details,
/// e.g. use an `Arc` instead of `Box`, or even inlining the progress.
pub struct DynProgress;

impl DynProgress {
    pub fn new<P: Progress>(p: P) -> Self;
}

impl Progress for DynProgress {
    type SubProgress = Self;

    // ...
}

trait Progress {
    // ...

    // Provided method
    fn into_dyn_progressive(self) -> DynProgress;
}

Features

For starters, I think we can put gix_negotiate, gix_ignore and gix_utils behind new feature flags.

Then we can put ability to fetch and update new repository into its own features since many crates might just want to discover local repository, e.g. vergen.

Motivation 🔦

As shown in cargo-bins/cargo-binstall#1304 (comment) , compilation of gix (not its dependencies) on GHA MacOS runner took more than 60s, which is a bit too long.

simple-git, which only uses a few gix interface, also takes 34s just to compile.

I believe this is caused by two factors:

  • too much monomorphization
  • too many unused code and dependencies pulled in

Progress

@NobodyXu NobodyXu added the enhancement New feature or request label Aug 19, 2023
@Byron Byron added acknowledged an issue is accepted as shortcoming to be fixed help wanted Extra attention is needed labels Aug 19, 2023
@Byron
Copy link
Member

Byron commented Aug 19, 2023

Thanks for bringing up ideas for a way forward to reducing compile times, it's very much appreciated and I think it's good to start this sooner than later.

Monomorphization

First of all, I agree that in most cases, monomorphization can be considered a convenience rather than a necessity (i.e. performance). Thus I believe that unless a function or closure isn't going to be called millions of times per second, there is no need to monomorphize it. Most methods and functions provided here fulfil this criterium. This affects most places where callbacks and the Progress trait is used.

about prodash and Progress

Thus I am very open to offering a &dyn compatible progress trait and all necessary refactorings to prodash can be done to accomplish this. I may say though that in the presence of threads all this gets a little more complicated and we may want to change set setters of Progress to use &self instead of &mut self, as most implementations internally are thread-safe anyway. prodash was originally designed so that each handle to a Progress implementation is handed to a thread, so the are not Clone themselves, so there now is a counter() method to workaround that. Maybe that can be cleaned up in the progress as well. Performance for Progress is critical, of course, but with the proposed optional dyn support we can have the best of both worlds.

Momo or not

I think it's worth making the process of monomorphization simpler and using a custom version of momo may help with that. I also hope that by using a source-only way of building it doesn't add too much compile time - for obvious reasons I don't want to have any kind of non-auditable blobs in the dependency chain until auditable blobs are supported by cargo natively. If there is doubt about its feasibility or difficulty, it's definitely fine to write the 'landing pads' by hand for the heavy-hitting functions.

About feature toggles

I'd love to start thinking about feature toggles in higher-level concepts, or components if you will. Like attributes or excludes or pathspecs. Each of these would be a feature toggle. This would also mean that features like fetch then require a certain set of components to work, and such methods might not be available unless the toggles are satisfied.
Given how painful feature toggles have been for me, I will be much more in touch with this part of the optimizations and would definitely be providing a set of components along with their dependencies, down to the plumbing crates that this affects.

Vergen

Yes, vergen will benefit from 'componentalization', even though one might also consider going all plumbing there. It won't get much better than that then, even though indeed I would hope that gix can be made so that most people still won't need to use plumbing just to get minimal compile times.

Testing of feature toggles

I think cargo-all-features would have to be used to assure everything works as intended - this will add CI time but we can probably just run it on its own machine to run in parallel to all the other ones. The budget of ~30min should be enough for that.

Let me thank you again for taking the lead in this 🙏 - I will support you as good as I can.

@NobodyXu
Copy link
Contributor Author

Thus I am very open to offering a &dyn compatible progress trait and all necessary refactorings to prodash can be done to accomplish this.

Thanks, I've opened Byron/prodash#22

I may say though that in the presence of threads all this gets a little more complicated and we may want to change set setters of Progress to use &self instead of &mut self, as most implementations internally are thread-safe anyway.

That can also be done easily,

prodash was originally designed so that each handle to a Progress implementation is handed to a thread, so the are not Clone themselves, so there now is a counter() method to workaround that. Maybe that can be cleaned up in the progress as well. Performance for Progress is critical, of course, but with the proposed optional dyn support we can have the best of both worlds.

Once Progress is changed to use immutable reference, perhaps we can change this to return AtomicUsize instead?

For sharing across threads, I think std::thread::Builder::spawn_scoped or rayon::in_place_scope could be used to avoid that Arc.

@Byron Byron mentioned this issue Aug 19, 2023
2 tasks
@Byron
Copy link
Member

Byron commented Aug 19, 2023

Once Progress is changed to use immutable reference, perhaps we can change this to return AtomicUsize instead?

It already does it, but it's using shared ownership. Doing so actually makes it easier to use as those who don't need ownership over the AtomicUsize which lies underneath can always borrow it to scoped threads if they wish. Changing this is probably more like a rewrite of what prodash represents.


I couldn't resit but to get an overview for myself of what's possible here, and I think it's a lot. For one, I already started the compartmentalization in gix by adding new but less used capabilities behind a feature toggle. This was easy as they were nicely separated. Others are more intertwined, but I think this map will help to plan where to make the cuts.

                                                                                                            ╔════════════════════╗
                                                 ┌─────────────────────────────────────────────────────────▶║     Submodules     ║
                                                 │                                                          ╚════════════════════╝
                                                 │                                                                     ▲                                                                           ╔════════════════════╗      ╔════════════════════╗
                                                 │                                                                     │                                                                           ║   Fetch Changes    ║◀──┬─▶║    Push Changes    ║
                                                 │                                                                     │                                                                           ╚════════════════════╝   │  ╚════════════════════╝
                                                 │                                                                     │                                                                                      ▲             │             ▲
                                                 │   ╔═══════════════════╗                      ╔══════════════╗       │                                                                                      │             │             │
                                                 │   ║gix-worktree-state ║                      ║  gix-status  ║       │                                                                                      │             │             │
                                                 │   ╚═══════════════════╝                      ╚══════════════╝       │                                                                                      │             │             │
                                                 │             ▲                                        ▲              │                          ┌────────────────────┐         ╔════════════════════╗       │             │             │
                                                 │             │                                        │              │                          │     imara-diff     │────────▶║     Blob-Diff      ║       │             │             │
                                                 │             │                                        │              │                          └────────────────────┘         ╚════════════════════╝       │             └─────────────┼────────────────────────┐
                                                 │             │                                        │              │                                                                    ▲                 │                           │                        │
                                                 │             │                                        │              │                                                                    │                 └──────────────┬────────────┘                        │
                                                 │             └─────────────────────┬──────────────────┘              └───────────────────────────────────────────────────────────────┐    │                                │                                     │
                                                 │                                   │                                                                                                 │    │                                │                                     │
                                                 │                                   │                                                ╔════════════════════╗  ╔══════════════════╗     │    │    ╔═══════════════╗           │                                     │
                                                 │                                   │                                                ║      Describe      ║  ║ Traverse Commits ║     │    │    ║   Tree-Diff   ║           │                                     │
                                                 │                                   │                                                ╚════════════════════╝  ╚══════════════════╝     │    │    ╚═══════════════╝           │                                     │
                                                 │                                   │                                                           ▲                      ▲              │    │            ▲                   │                                     │
    ╔════════════════════╗              ╔════════════════╗                 ╔═══════════════════╗                                                 │                      │              │    │            │                   │                                     │
    ║    gix-archive     ║              ║ gix-submodule  ║        ┌───────▶║   gix-worktree    ║◀──────────┐                                     │                      │              │    │            │                   │                                     │
    ╚════════════════════╝              ╚════════════════╝        │        ╚═══════════════════╝           │                                     │                      │              │    │            │                   │                                     │
               ▲                                 ▲                │                  ▲                     │                                     ├──────────────────────┼──────────────┼────┴────────────┤                   │                                     │
               │                                 │                │                  │                     │                                     │                      │              │                 │                   │                                     │
               │                                 │                │                  │                     │                                     │                      │              │                 │                   │                                     │
               │                                 │                │                  │                     │                                     │                      │              │                 │                   │                                     │
               │                                 │                │                  │                     │                                     │                      │              │                 │                   │                                     │
               │                                 │                │                  │                     │                                     │                      │   ╔════════════════════╗       │         ╔═══════════════════╗                           │
               │                                 │                │                  │                     │                                     │ ┌────────────────────┼──▶║     Repository     ║───────┼────────▶║      Remote       ║                           │
               │                                 │                │                  │                     │                                     │ │                    │   ╚════════════════════╝       │         ╚═══════════════════╝                           │
 ╔═══════════════════════════╗          ╔════════════════╗        │                  │           ╔═══════════════════╗    ╔═══════════════════╗  │ │                    │              ▲                 │                   ▲                                     │
 ║    gix-worktree-stream    ║          ║  gix-pathspec  ║◀─┐     │         ┌────────┴──────┐    ║     gix-index     ║    ║   gix-revision    ║──┘ │                    │              │       ╔═══════════════════╗         │                                     │
 ╚═══════════════════════════╝          ╚════════════════╝  │     │         │               │    ╚═══════════════════╝    ╚═══════════════════╝    │                    └──────────────┼───────║   gix-traverse    ║         │                                     │
               ▲                                 ▲          │     │         │               │                                                      │                                   │       ╚═══════════════════╝         │                                     │
               │                                 │          │     │         │               │                                                      │                                   │                                     │                                     │
               │                                 │          │     │         │               │                                                      │           ┌───────────────────────┼───────────────────────┐             │                                     │
               │                                 │          │     │         │               │                                                      │           │                       │                       │             ├────────────┐                        │
               │                                 │          │     │         │               │                                                      │           │                       │                       │             │            │                        │
               │                                 │          │     │         │               │                                                      │           │                       │                       │             │            │                        │
               │                                 │          │     │         │               │                                                      │           │                       │                       │             │            │                        │
               │                                 │          │     │         │               │                                                      │           │                       │                       │             │            │                        │
               │                        ╔════════════════╗  │     │  ╔════════════╗  ╔════════════╗                                                │           │                       │                       │             │            │                        │
               └────────────────────────║ gix-attributes ║──┼─────┴─▶║ gix-filter ║  ║ gix-ignore ║                                                │ ╔═══════════════════╗      ╔════════════╗      ╔═════════════════════╗  │            │             ╔════════════════════╗
                                        ╚════════════════╝  │        ╚════════════╝  ╚════════════╝                                                │ ║  Object Storage   ║      ║ References ║      ║    Configuration    ║  │  ╔═══════════════════╗   ║    gix-protocol    ║
                                                 ▲          │                               ▲                                                      │ ║     (gix-odb)     ║      ║ (gix-ref)  ║      ║    (gix-config)     ║  │  ║  gix-credentials  ║   ╚════════════════════╝
                                                 │          │                               │                                                      │ ╚═══════════════════╝      ╚════════════╝      ╚═════════════════════╝  │  ╚═══════════════════╝              ▲
                                                 │          │                               │                                                      │           ▲                       ▲                                     │                                     │
                                                 │          │                               │                                                      │           │                       │                                     │                                     │
                                                 │          │                               │                                                      │           │                       │                                     │                                     │
                                                 │          │                               │                                                      │           │                       │                           ╔═══════════════════╗               ╔════════════════════╗
                                                 │          │                               │                                                      │           └───────────────────────┤                           ║    gix-refspec    ║               ║   gix-transport    ║
                                                 │          │                               │                                                      │                                   │                           ╚═══════════════════╝               ╚════════════════════╝
                                          ╔═════════════╗   │                               │          ╔═══════════════════╗                       │                                   │                                                                          ▲
                                          ║  gix-glob   ║───┴───────────────────────────────┘          ║    gix-mailmap    ║                       │                                   │                                                                          │
                                          ╚═════════════╝                                              ╚═══════════════════╝                       │                                   │                                                   ┌──────────────────────┼────────────────────────┐
                                                                                                                                                   │                                   │                                                   │                      │                        │
                                                                                                                                      ╔═════════════════════════╗      ╔══════════════════════════════╗                                    │                      │                        │
                                                                                                                                      ║  Repository Discovery   ║      ║      Temp and Lockfiles      ║                           ╔═════════════════╗ ╔═══════════════════════╗ ╔════════════════════╗
                                                                                                                                      ║     (gix-discover)      ║      ║  (gix-tempfile & gix-lock)   ║                           ║  Ssh Transport  ║ ║  Git Local Transport  ║ ║  Https-Transport   ║
                                                                                                                                      ╚═════════════════════════╝      ╚══════════════════════════════╝                           ╚═════════════════╝ ╚═══════════════════════╝ ╚════════════════════╝





┌───────────────────────────────────────────────────────────┐       ┌──────────────────────────────────────────────────────────┐        ┌──────────────────────────────────────────────────────────┐       ┌─────────────────────────────────────────────────────────────────────────┐
│                                                           │       │                                                          │        │                                                          │       │                                                                         │
│               _                         _   _             │       │            _                  _     _           _        │        │               _             _               _            │       │              _              __           _                              │
│              (_)                       | | | |            │       │           (_)                | |   (_)         | |       │        │              (_)           | |             | |           │       │             (_)            / _|         | |                             │
│          __ _ ___  ________ _ __   __ _| |_| |__          │       │       __ _ ___  ________ ___ | |__  _  ___  ___| |_      │        │          __ _ ___  ________| |__   __ _ ___| |__         │       │         __ _ ___  ________| |_ ___  __ _| |_ _   _ _ __ ___  ___        │
│         / _` | \ \/ /______| '_ \ / _` | __| '_ \         │       │      / _` | \ \/ /______/ _ \| '_ \| |/ _ \/ __| __|     │        │         / _` | \ \/ /______| '_ \ / _` / __| '_ \        │       │        / _` | \ \/ /______|  _/ _ \/ _` | __| | | | '__/ _ \/ __|       │
│        | (_| | |>  <       | |_) | (_| | |_| | | |        │       │     | (_| | |>  <      | (_) | |_) | |  __/ (__| |_      │        │        | (_| | |>  <       | | | | (_| \__ \ | | |       │       │       | (_| | |>  <       | ||  __/ (_| | |_| |_| | | |  __/\__ \       │
│         \__, |_/_/\_\      | .__/ \__,_|\__|_| |_|        │       │      \__, |_/_/\_\      \___/|_.__/| |\___|\___|\__|     │        │         \__, |_/_/\_\      |_| |_|\__,_|___/_| |_|       │       │        \__, |_/_/\_\      |_| \___|\__,_|\__|\__,_|_|  \___||___/       │
│          __/ |             | |                            │       │       __/ |                       _/ |                   │        │          __/ |                                           │       │         __/ |                                                           │
│         |___/              |_|                            │       │      |___/                       |__/                    │        │         |___/                                            │       │        |___/                                                            │
│                                                           │       │                                                          │        │                                                          │       │                                                                         │
└───────────────────────────────────────────────────────────┘       └──────────────────────────────────────────────────────────┘        └──────────────────────────────────────────────────────────┘       └─────────────────────────────────────────────────────────────────────────┘

                                                                                       ┌─────────────────────────────────────────────────────────────────────────────┐
                                                                                       │                                                                             │
                                                                                       │             _                              _        _   _ _                 │
                                                                                       │            (_)                            | |      | | | (_)                │
                                                                                       │        __ _ ___  ________ _ __   __ _  ___| | _____| |_| |_ _ __   ___      │
                                                                                       │       / _` | \ \/ /______| '_ \ / _` |/ __| |/ / _ \ __| | | '_ \ / _ \     │
                                                                                       │      | (_| | |>  <       | |_) | (_| | (__|   <  __/ |_| | | | | |  __/     │
                                                                                       │       \__, |_/_/\_\      | .__/ \__,_|\___|_|\_\___|\__|_|_|_| |_|\___|     │
                                                                                       │        __/ |             | |                                                │
                                                                                       │       |___/              |_|                                                │
                                                                                       │                                                                             │
                                                                                       └─────────────────────────────────────────────────────────────────────────────┘

It may look overwhelming, layout is hard 😅. These big crates at the bottom are basically foundation, they are always pulled in one way or another. Everything prefixed with gix- is a crate that also represents a component.

@Byron
Copy link
Member

Byron commented Aug 20, 2023

I thought about monomorphization a little more and after consulting with @joshtriplett (thank you 🙏) came to a conclusion that I find exciting enough to share.

First of all, what are the costs of monomorphism? For the Rust compiler, at least the simple way they are used here, there is no significant overhead, so cargo check will run just as fast as it would with everything being polymorphic.
The real cost of generics occours when they are instantiated multiple times as this multiples the work LLVM has to do during cargo build.

With this in mind, and knowing that monomorphism is best for performance in this very performance-minded crate-ecosystem, and also good for convenience, I arrived at the following conclusions.

  • plumbing crates, i.e. gix-*, should not polymorphize unless their own function calls are unnecessarily duplicated. If that's the case, probably detectable with cargo bloat, this should be fixed rather than polymorphising it.
    • If plumbing crate functions are prone to duplication due to calls from other plumbing crates, and performance is not a concern for the call, use use the generic function as a landing pad and forward it to a dyn trait version - let's call this momo from now on.
  • the gix crate is assumed to primarily use generics for convenience and should absolutely avoid duplications of functions and methods. For that reason, momo should be use generously to retain the convenience of generics without causing (much) bloat. I assume that in most cases, performance won't matter and we can easily pay for the dyn overhead.

Since gix is the entry-point for most it seems to make sense to focus efforts there initially. I also hope that there will be a way to measure the effects of (probably unnecessary) function duplication at build time due to monomophism and use that as driver to fix these in the entire gix-* ecosystem.

I invite everyone involve to let me know their thoughts or point out mistakes or other flaws that ought to be improved. After all, I am threading on new grounds here. Thank you🙏.

@Byron Byron added the C-tracking-issue An issue to track to track the progress of multiple PRs or issues label Aug 22, 2023
@NobodyXu
Copy link
Contributor Author

@Byron Another advice for speeding up compilation of gitoxide itself:

I think it might be a good idea to support multi-call binary that can do both gix and ein and uses args[0] (symlink or hardlink name) to tell which one to call.

This would reduce:

  • link time of gitoxide since it doesn't have to generate and link two binaries
  • size of the executable generated

@Byron
Copy link
Member

Byron commented Sep 2, 2023

That's a good idea! It would check what is name is and then act differently depending on that.

I think from a setup and installation perspective, it's easiest to declare to binaries with the same path so cargo will create two full-size binaries (and install them) at the compile-time price of one. ein and gix are currently 9MB and 18MB in size respectively, and that would add 9MB to the installation size, but I find that acceptable.

@Byron
Copy link
Member

Byron commented Sep 2, 2023

I spiked it and think that doing so only benefits installation if links are used, overall compile time should then be lower, as well as the overall size on disk.

However, for CI it would then take longer as it rebuilds gix with varying compile flags, and to do that it would then need to compile ein (or its code) as well. I kept the experiment in the codebase though as I think it one day has a place.

Byron added a commit that referenced this issue Sep 2, 2023
This increases installation size, but should decrease build time
as symbols and optimizations can be shared more.

Once deduplication of generics has happened, these wins should increase
even more.

However, as it probably increases compile times for CI as it will take longer
to build each binary with different compile time flags. Thus I just leave
the `uni.rs` file for reference.
@NobodyXu
Copy link
Contributor Author

NobodyXu commented Sep 5, 2023

New idea:

I will open a separate PR to de-momo functions using TryInto since there are only a few of them and they might use .map_err, which is really hard to parse.

I also found several functions using impl FnMut(...) and wonder if it makes sense to use a &mut dyn FnMut(...) instead.

Originally posted on #992 (comment)

P.S. I'm really busy recently, would definitely get around to gix-macros

@Byron
Copy link
Member

Byron commented Sep 6, 2023

I will open a separate PR to de-momo functions using TryInto since there are only a few of them and they might use .map_err, which is really hard to parse.

Maybe it's OK to just save the time, as these functions could also be de-momo'ed by hand. What's much more important to make momo usable is to let it bark if it's ineffective. Thinking about it, this should even go as far as to let it fail if it doesn't completely remove all generics, because that's the whole purpose.

I also found several functions using impl FnMut(...) and wonder if it makes sense to use a &mut dyn FnMut(...) instead.

I thought the same and have done a lot of work in that regard in this PR.

The intermediate results are in and in terms of compile time, there is no change at all 😩.

dynified many plumbing crates
-rwxr-xr-x  1 byron  staff  66476417 Sep  6 09:21 target/debug/gix
-rwxr-xr-x  1 byron  staff  18937185 Sep  6 09:22 target/release/gix
main - no change
-rwxr-xr-x  1 byron  staff  68463521 Sep  6 09:23 target/debug/gix
-rwxr-xr-x  1 byron  staff  18912993 Sep  6 09:23 target/release/gix

Only the debug-binary sizes show some promise at least, but everything else is still unchanged.

It seems like it's a lot of work for nothing more than a few scraps :/.

I will continue with the dynification of the plumbing crates as I think it's the right way to go in order to prevent duplication in binaries. I also made a couple of additional simplifications that improve things, so overall it's worth keeping even though there isn't much effect at least with dynifying plumbing crates. Maybe momo in gix will make a slightly more meaningful difference.

This probably also means that compartmentalizing gix via feature toggles will be the biggest win. In imagine the minimal version will just be able to open repos and interact with objects, configs and refs. Everything else is behind respective feature toggles.

@NobodyXu
Copy link
Contributor Author

NobodyXu commented Sep 6, 2023

Maybe it's OK to just save the time, as these functions could also be de-momo'ed by hand. What's much more important to make momo usable is to let it bark if it's ineffective.

That's exactly my thought, I want to keep mono simple and maintainable and de-momo TryInto by hand since it is a bit too complex to do it in proc-macro.

The intermediate results are in and in terms of compile time, there is no change at all 😩.

I think we should do the benchmark on GHA since this is the main blocking when creating and updating a PR, especially Windows and MacOS, which are much slower than the Linux machine with less cores.

Not to mention MacOS has security checking turned on by default which slow down I/O and add more CPU usage, Windows might also have defender and virus scanning turned on and its disk I/O is also not as good as Linux.

Only the debug-binary sizes show some promise at least, but everything else is still unchanged.

This could be useful on GHA where maximal size for a single cache is 10G after compression using zstd (unless you compress the data yourself before passing to @actions/cache)

It seems like it's a lot of work for nothing more than a few scraps :/.

Definitely not true, I believe our effort would add up and positively affect the GHA CI time and local dev.

This probably also means that compartmentalizing gix via feature toggles will be the biggest win.

Yes, it definitely will be the most impactful.

In imagine the minimal version will just be able to open repos and interact with objects, configs and refs.

I think even objects interaction can be behind feature flags, since many would just hit the checked out worktree directly.

@Byron
Copy link
Member

Byron commented Sep 6, 2023

I think we should do the benchmark on GHA since this is the main blocking when creating and updating a PR, especially Windows and MacOS, which are much slower than the Linux machine with less cores.

In the end, this is what matters. However, I think that local build times relate strongly to GHA build times, and a crate taking half the time to compile locally should take half on GHA as well. Not seeing any improvements locally is quite an indicator that nothing great can be expected on GHA either.

Definitely not true, I believe our effort would add up and positively affect the GHA CI time and local dev.

Let's wait for GHA - as of now the data I gathered suggests it's not worth it, at least from that point of view. From another point of view, it's valuable as the method signatures seem simpler while avoiding duplication in the final binary.

I think even objects interaction can be behind feature flags, since many would just hit the checked out worktree directly.

There is also practical concerns - features interact with each other and it's simply not feasible to offer something like "want objects, but not refs, this, but not that" - it's going to make me crazy (I have been there 😅). Feature toggles must be well-chosen to optimize for typical use-cases, only the most important of them. Some features are orthogonal though, and these are mix-and-match, but for basics like objects, these just need to be there. Feature toggles are a huge burden, and not a single one will be added without knowing that it has high-enough value.

@NobodyXu
Copy link
Contributor Author

NobodyXu commented Sep 6, 2023

In the end, this is what matters. However, I think that local build times relate strongly to GHA build times

One important difference to note is that, GHA machines are much less powerful than a dev's own laptop, and they are heavily overloaded so even a tiny bit of improvements that can be ignored on the local build can turn out to be a noticeable win on GHA.

GHA's default runner:

  • Windows & Linux: 2 (old and inefficient) x86_64 virtual cores, 7G RAM, 14G SSD
  • MacOS get 1 more x86_64 core and 7G more RAM

The CPU for Windows & Linux is not only old, but also server CPUs which has weaker single-core perf in exchange for better scale-up, while the MacOS x86_64 is known to have heating issues and slow until M1 comes along...

So I don't think running locally is good indicator for GHA, the changes could have much more effect on GHA than locally.

Let's wait for GHA - as of now the data I gathered suggests it's not worth it, at least from that point of view.

If the data from GHA is insignificant, then it's probably insignificant in reality.

Though the reduced binaries is a good thing consider GHA only provides 14G of storage and 10G of cache.

There is also practical concerns - features interact with each other and it's simply not feasible to offer something like "want objects, but not refs, this, but not that" - it's going to make me crazy (I have been there 😅)

That's true, I didn't consider that.

@Byron
Copy link
Member

Byron commented Sep 6, 2023

I apologize for having been so negative - this was an expression of my disappointment, having expected massive wins but getting only marginal ones.

Having thought about this more, it's clear now that Rust is actually pretty good and fast when handling with typical-use generics. This comes at no surprise as after all, the standard library is full of it. It's also what makes it useful and convenient while delivering optimal performance.

With the learnings I am now going through it all again and take a more differentiated approach. Small methods, especially those with just a single generic type which doesn't have too many possible source types, can stay generic as the total amount of cost through duplication is small - a price worth paying. However, when the methods get larger, I while still wanting the extra convenience, I put in an inner function myself to limit the amount of code generated.

Finally, impl Fn/FnMut will now always be dyn to avoid the worst-case duplication scenario.

In the end, if nothing else, it sets a nice precedent for the codebase to maintain from now on. The reason I like it in particular is that cargo does it very similarly, they are very conscious about generics and I liked it.

@NobodyXu
Copy link
Contributor Author

NobodyXu commented Sep 6, 2023

I apologize for having been so negative - this was an expression of my disappointment, having expected massive wins but getting only marginal ones.

I'm also a bit disappointed here, though optimizing compile-time ia indeed hard problems.

With the learnings I am now going through it all again and take a more differentiated approach. Small methods, especially those with just a single generic type which doesn't have too many possible source types, can stay generic as the total amount of cost through duplication is small - a price worth paying. However, when the methods get larger, I while still wanting the extra convenience, I put in an inner function myself to limit the amount of code generated.

I think you can still use momo for where it can apply since it's more readable.
TryInto would have to be manually done though and the fetching and checkout method takes a TryInto, which is probably more important than rest of the methods since they are always used on cloning.

Finally, impl Fn/FnMut will now always be dyn to avoid the worst-case duplication scenario.

👍 though FnOnce is indeed a hard problem that needs rustc to support "move-by-ref" and better Unsize support.

For now, we can create a wrapper (NOTE that I wrote this on my iPad and haven't run through miri and is just a proof of concept):

struct FnOnceDynWrapper<F>(Option<F>);
impl FnOnceDynWrapper<F> where F: FnOnce() {
    fn to_dyn_callable(&mut self) -> Option<DynFnOnce<'_>> {
        if self.0.is_some() {
            Some(DynFnOnce {
                data: self as *mut _ as *mut (),
                f: |data| {
                    let data = unsafe { &mut *(data as *mut Self) };
                    (data.take().unwrap())()
                },
                phantom: PhantomData,
            })
        } else {
            None
        }
    }
}
struct DynFnOnce<'f> {
    data: *mut (),
    f: fn(*mut ()),
    phantom: PhantomData<&'f mut ()>,
}
impl DynFnOnce<'f> {
    fn call(self) {
        (self.f)(self.data)
    }
}

In the end, if nothing else, it sets a nice precedent for the codebase to maintain from now on. The reason I like it in particular is that cargo does it very similarly, they are very conscious about generics and I liked it.

Yeah, that's unfortunate that generics have a cost, but there's no free lunch (unless rustc implements parallel frontend or other improvments).

@Byron
Copy link
Member

Byron commented Sep 6, 2023

👍 though FnOnce is indeed a hard problem that needs rustc to support "move-by-ref" and better Unsize support.

You obviously know a lot about these things, and maybe there are solutions the problems I couldn't resolve yet.

The bigger picture is for<'a> FnMut(&gix_hash::oid, &'a mut Vec<u8>) -> Result<Something<'a>, E> where E: Error + Send + Sync + 'static, which is the signature of functions that help abstract object database access. These are used to not have to depend on on gix-odb just to access objects, and generally such closures are used to keep plumbing crates separate.

An annoying, but probably surmountable problem is the error type. By now I am avoiding having generic errors, and instead just box the errors returned by these closures. Unfortunately, these Box<dyn Error + Send + Sync> don't interact well. First of all, these cannot be nested, secondly, and probably for the same reason, one cannot use ? anymore, particularly not with anyhow. Often one can paper over this issue), but sometimes it's very frustrating. For example, while trying to reduce the amount of generics I wanted to 'cut' the chain by using &mut dyn FnMut() -> Result<_, Box<dyn…>> when possible. The call stack became generic1 -> dyn1 -> generic2 -> dyn2, and since generic1 used Result<_, E> that was degenerated to Box<dyn Error> in dyn1, it was now impossible to call into generic2 which needed E: Error again. That's probably good as we wouldn't want to double-box things, but it's clearly unsolved problem for composability.

The next problem for which I have absolutely no feasible workaround is that those who use threads to parallelize object access need the FnMut(…) -> … + Send + Clone. The Send + Clone bound is preferred over Send + Sync as this naturally makes thread-local storage possible and convenient. But unfortunately there is now way to have a BoxClone<dyn FnMut>. This means one can only make a 'cut' somewhere where per-thread code is executed, but it's possible to run into the first stated problem then.

One solution I am contemplating with to at least solve the generic1 -> dyn1 -> generic2 -> dyn2 problem is to use a distinct type Box<dyn Error> instead of E in the generic landing-pad. Since these error types are the same everwhere, it should work.

But I don't like it very much and will probably just keep it generic for now for a lack of indication that it actually makes a meaningful difference.

Next up is to try more granular feature toggles in gix.

@NobodyXu
Copy link
Contributor Author

NobodyXu commented Sep 7, 2023

First of all, these cannot be nested, secondly, and probably for the same reason, one cannot use ? anymore, particularly not with anyhow.

Is it reasonable to have a gix-error crate where it has a newtype over Box<dyn Error + Send + Sync> and provides various convenience function and From impl?

We could also add an ErrorKind for gix and a message field to it for more readable error messages so that we can avoid creating new error enums using thiserror.

But unfortunately there is now way to have a BoxClone<dyn FnMut>.

I think it might be possible

trait DynClonable {
    type Target;
    fn clone(&self) -> Self::Target;
}

impl DynClonable for S { // Assume S implements clone
    type Target = Box<dyn DynClonable + ...>;

    fn clone(&self) -> Self::Target {
        Box::new(Clone::clone(self))
    }
}

And then we can add a new proc-macro to gix-macros:

#[derive(DynClonable(Box<dyn DynClonable + Send + ...>))]
struct S { ... }

Though I have to finish momo first, and then I will have time for this one.

Rant on dtolnay's absolute magic yet fragile dyn-clone crate that has no warning against potential breakage in new rustc:

I was searching on libs.rs and found dyn-clone.

I was quite happy but I decided to read the source code to understand how this magic crate works and found it using a fragile hack that can be broken by rustc at anytime.

And given that author is David Tolnay and all his recent history and my experience with his crates, I now found it hard to continue unconditionally trusting his crates and probably better to avoid them unless necessary.

I used his crate gh-token in cargo-binstall and turns out he didn't review the PR contributed carefully and introduced a bug in binstall (it executes gh auth token to get GitHub token since it could be stored encrypted, but that PR forgot to trim the newline and it's obvious that he did not pay much effort to review it or run it). Admittedly I should also spend sometime reviewing that PR myself given relevance to binstall, now we switch to run gh auth token directly in binstall since it is so simple and we can fix anything much faster that way.

@Byron
Copy link
Member

Byron commented Sep 7, 2023

I think this is a stroke of genius! But let me explain!

Having a gix-error crate was a strong itch after the recent drama when I thought about removing thiserror as a dependency. I also start having problems with these dtolnay maintained foundation crates as they serve their own interests without showing consideration for their position in the ecosystem. My particular gripe is the seeming unwillingness to solve inconveniences that should be solvable without explanation or further interaction. But since the general opinion has been made clear (you don't like it, fork it), I think that is something a crate-ecosystem like gix-* is in a good position to do. After all, the costs amortise over dozens of crates.

But even with that said, I wouldn't want to fork thiserror on those grounds alone, I was talking about inconveniences after all. And now comes the twist: what if gix-error would be thiserror with a killer feature?

We could also add an ErrorKind for gix and a message field to it for more readable error messages so that we can avoid creating new error enums using thiserror.

This is always a point of contention as these are two different and incompatible paradigms. One error to rule them all, which is easy to use if you want to forget about it, but hard to use if you actually want to check for particular errors, as any error is possible and there is no compile time protection.

But what if one could chose the error style at compile time?

Truth to be told, I think treating errors like gix* does is the best way simply because it doesn't degenerate information while keeping a tree of possible errors for the called function or method. For the author, that's extra work, but it's manageable thanks to thiserror. However, for callers it can be a burden, and it's probably visible as footprint as well.

What if there was a compile-time feature in gix-error that changes the error style to the-one-error? Within a crate, it could generate an enum with all seen error variants, and bake the error message into a string when the error is instantiated. This way, everything would stay as is, but now it's possible to get git2-style errors.

And yes, an anonymous error could then be struct Error(#[source] pub Box<dyn Error + Send + Sync>) to paper over inconveniences with Box<dyn Error…>, it should all be more more solvable once there is a shared error type.

And here comes another twist: You would have to be the one to do it 😅 - I really am no good for proc-macros just yet.

What do you think?

@niklaswimmer
Copy link
Contributor

Would this not cause problems with libraries that depend on gix? My thinking is that if a library does not enable the one-error-only feature and depends on the existence of multiple error variants (for matching eg) all users of that library can then no longer use the one-error-only feature? Or am I misunderstanding something?

@Byron
Copy link
Member

Byron commented Sep 7, 2023

That's a valid point, thanks for bringing it up (I didn't think of it)! The end-user should be the one who choses, and those in-between libraries would then not be allowed to rely on capabilities of these errors that might not exist in the final application, as there can only be one.

The baseline for matching on an error would then have to be that basic one-for-all enum Kind, as this can (and probably should) be provided for the detailed version (aka thiserror) as well. Thus library authors would only be allowed to rely on that, and could never match on detailed errors. This probably means that the default is to not have detailed errors as well.

Thinking further, this probably means that such an error should really be specific to the gix* crates as it wouldn't bode well to have a 'split' ecosystem around it as people use incompatible options.

But maybe that isn't a problem if it's made clear that the only proper usage is to enable detailed-errors (aka be like thiserror) in the application itself, the root of the dependency tree.

@NobodyXu
Copy link
Contributor Author

NobodyXu commented Sep 7, 2023

My concern with the-one-error is same as @niklaswimmer : it would be breaking code since cargo unifies the features.

However I do get what you are proposing and I think we can do it in reverse: Have an "error-tree" feature which implements a function on the gix_errors::Error to obtain the tree of errors:

enum ErrorTreeNode<'a> {
    Node(&'a gix_error::Error),
    Leaf(&'a dyn Error + Send +Sync),
}
fn get_error_tree_node(&self, node_name: &str) -> Option<ErrorTreeNode<'_>>;

The node name will be something like "clone.fetch.http".

This would avoid all the annoying and repeated use of thiserror just to generate different kinds of error in a lot of mods.

Alternatively, a provider-like function would also be very helpful in traversing deep-tree of errors: rust-lang/rust#96024

@Byron
Copy link
Member

Byron commented Sep 7, 2023

❤️

This is even more bold than I originally stated (if I understand it correctly) as it would be much like anyhow, but for libraries. As long as it's possible to assign a fixed variant name so people know what to look for in the tree, I'd have no concerns. After all, it does take considerable effort to maintain all these error variants by hand and I'd love to have some more automation.

I invited you to therror which I name-grabbed because I couldn't resist, as I think a nicer kind of error handling is general enough to warrant its own repository.

Lastly, I think that using thiserror syntax as a basis is possible and it would definitely help adoption within this codebase alone. From there, additional features could be added, maybe even breaking ones that essentially do away with the actual variants and replace everything with an error tree. Maybe all this can, somehow, work together to provide a way to not have to degenerate passed in error context into a string on instantiation time for those who need it, or as a means to be more efficient.

Thinking about it, this probably means the therror would be compatible to thiserror for declaration, but it's optional and the final error type would always be the same. This means for migration, one could just make this special error types private, but keep them. New methods would then use the new error handling syntax that therror also provides.

I think technically, all this can work, but it also sounds like a lot of it 😅. Can this be done, or is it a pipe-dream? Because one thing is for sure - I wouldn't be the one to do it.

PS: I am happy to transfer full ownership over therror to the one implementing it, both the repo as well as the crates.

@NobodyXu
Copy link
Contributor Author

NobodyXu commented Sep 7, 2023

Thanks @Byron I had accepted the invitation, though I have to finish the gix-macors PR first.

We would have to try this before knowing if it would work.

Though I do think that keep making new enums is not sustainable for large codebase, we hit the same limitation in cargo-binstall https://github.com/cargo-bins/cargo-binstall/blob/b36674a1c4f14211e70998bde91b23e7d7f1d15d/crates/binstalk/src/errors.rs#L56

We use mette and thiserror for error with helps and it's extra hard to remove them due to the convenience of proc-macro.

Maintenance of such large enum isn't easy since some error types are so large and it requires boxing and we have to manually implement From impl for these types.

If the error types are too large, then it would cause extra codegen and perf issues due to having to copy them around the stack and it often also occupies unused space on stack.

Combined with async which often has problems optimizing out copying, it makes it worse by also enlarging the future auto-generated.

@Byron
Copy link
Member

Byron commented Sep 7, 2023

Thanks @Byron I had accepted the invitation, though I have to finish the gix-macors PR first.

Great, thanks so much!

Though I do think that keep making new enums is not sustainable for large codebase, we hit the same limitation in cargo-binstall https://github.com/cargo-bins/cargo-binstall/blob/b36674a1c4f14211e70998bde91b23e7d7f1d15d/crates/binstalk/src/errors.rs#L56

I agree that once these grow, it becomes unfeasible. The 'higher' the level of function to call, the more errors are contained in that tree, possibly with multiple boxes within it to reduce stack size. But, I also think that up to some size, it's feasible, doable, and a 'perfect' representation of what can happen, even though it's not accessible by traversal or more generic means of accessing the tree.

And this brings me another possible insight that makes me feel even better about this. What if the thiserror-style version of an error is just another representation of the underlying tree? The tree-API can also be provided by such en enum-error, and it could also allow itself to be auto-converted to the 'base-error' which provides access to the generic tree only.

This is how high-level functions can opt to use the generic version of the tree right away, and the underlying methods that it calls may provide enum-style/thiserror-style errors, but these easily convert with ? while retaining their tree as completely as the data-model allows.

Speaking of data-model - maybe initially it is 'simple' with a path to the node in the tree, along with a string that was generated as message, for example based on the #[error("{format}")] of the thiserror or the new syntax that can bail out of any place in a method while providing such context. But once the provider-API is getting ready, it could be possible to also access the underlying values that were used to generate the message. Once that is there, enum-style errors can transform themselves to the base-error without any loss of information.

To me all this seems very feasible to happen over time, and technically it seems more than possible and a natural addition to what thiserror introduced, solving problems that came with it as it was used at scale.

And yes, it is exciting to see how this could also be used in cargo-binstall, which would certainly avoid overfitting therror to just the needs of gix*-crates.

@NobodyXu
Copy link
Contributor Author

NobodyXu commented Sep 7, 2023

And this brings me another possible insight that makes me feel even better about this. What if the thiserror-style version of an error is just another representation of the underlying tree? The tree-API can also be provided by such en enum-error, and it could also allow itself to be auto-converted to the 'base-error' which provides access to the generic tree only.

This is how high-level functions can opt to use the generic version of the tree right away, and the underlying methods that it calls may provide enum-style/thiserror-style errors, but these easily convert with ? while retaining their tree as completely as the data-model allows.

👍 that is probably how we are going to approach these problems.

Speaking of data-model - maybe initially it is 'simple' with a path to the node in the tree, along with a string that was generated as message, for example based on the #[error("{format}")] of the thiserror or the new syntax that can bail out of any place in a method while providing such context. But once the provider-API is getting ready, it could be possible to also access the underlying values that were used to generate the message. Once that is there, enum-style errors can transform themselves to the base-error without any loss of information.

We can already provide that information with Error::source.
It's less ergonomic than provider but still good enough for users who need them.

@Byron
Copy link
Member

Byron commented Sep 8, 2023

I have added as many feature toggles as possible and will cut a new release now. The recommendation is to go with default-features = false and then re-add all components that are actually required. With that, it should be possible to reduce dependencies and compile time.

It's a little comical that these measures improve everyones CI times except for the ones of gitoxide itself, as it will now have to test more features :D. That dramatization aside, the actual CI time is about the same with 31m per run.

@nathaniel-brough
Copy link
Contributor

Just as a bit of a note. I've found that setting the linker to mold makes a huge difference, in overall speed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
acknowledged an issue is accepted as shortcoming to be fixed C-tracking-issue An issue to track to track the progress of multiple PRs or issues enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

4 participants