no artificial intelligence, only human stupidity (specifically mine)
Hey everyone,
I am hereby announcing the first functional version of nix-weather-hs. This project is inspired and based on nix-weather by @cafkafk. For details see the README.org, but the brief version is that nix-weather-hs will check cache availability for a whole closure, build a graph in memory and then it allows you to export said graph into multiple formats. Before export there are a few filters available, which make the resulting graphs more readable.
As an example you can use the following command to check the availability of a NixOS configuration, reduce the graph to the 2nd level leaves of the uncached portion.
nix-weather-hs graph --filter 'uncached' --filter 'leaves@2' --output dot $(nix eval --raw .#nixosConfigurations.nixos.config.system.build.toplevel.drvPath)
As you can see, nix-weather-hs currently works with derivations, which is a defining feature compared to nix-weather. My version was specifically built to debug cases, where you are unable to even built the derivation, due to resource limits or other reasons.
If you want to contribute, open issues, you should be able to log into my Forgejo with GitHub (codeberg planned). If any issues arise, please ping me.
If you want to reach out privately, to talk about, well anything, please don’t hesitate to contact me here, on matrix @magic_rb:matrix.redalder.org or Signal magic_rb.01. If you’re considering asking an LLM a question about this project, please ask me instead.
Thank you for your time and hopefully nix-weather-hs is useful to you.
20 Likes
This is so cool!! Any plans to support arbitrary caches? i.e. caches other than cache.nixos.org
2 Likes
Yes, i just havent gotten around to writing the code 
I’ve got a mental block when it comes to finishing things, so i wanted to get this out as soon as possible.
Will write the code for caches maybe tomorrow (its really not a lot of code).
2 Likes
This is cool thanks for sharing!
One question I have about both your project and the inspiration is that (I think) the requisites of drvPath are generally all the build inputs, which can be a larger graph than the run-time inputs. Does nix-weather-hs or nix-weather try to distinguish whether a build-time or run-time expansion of each reference is appropriate? I definitely could be mistaken – I was just diving into eval/build stuff recently and this was the imperfect understanding I had come to at this point.
For example, obviously my system itself will not be cached (e.g. config.system.build.toplevel), so since I need to build I would check the referencesof it’s drvPath, which may refer to foo.drv with an output foo which is cached.Since foo is cached, the references of foo.drv shouldn’t be relevant to me right? I haven’t checked, but is your project or the inspiration walking the tree initiated by top-level.drv node-by-node, and deciding whether or not to expand each reference.drv depending on whether or not reference is cached?
3 Likes
Yeah so currently the expansion algorithm is naive. It assumes that all buildtime references become runtime references.
In an ideal world the expansion would be truly hybrid, where derivations who’s outputs are available, onky the runtime dependencies (so more the runtime dependencies of the derivations outputs) become children. And for derivations which have not been built yet, we make the conservative assumption that all build time dependencies, become runtime dependencies.
If you want to try using nix-weather-hs on a output path, you may be able to switch the function in Main.hs from doHybrid to doSimple. I think the signatures diverged a bit, but its not a hard fix.
I’ll add making sure this works with output paths onto my TODO list for tomorrow 
EDIT: the “hybrid” approach is exactly what you describing. It complicates things a bit, not 100% sure how to do it yet.
3 Likes
@samiser multiple caches are now implemented. It does a OR between them and checks them in sequence for each store object. So parallelism only occurs between store objects.
Verified by checking availability against my CIs cache and NixOS cache, which gave me 100% coverage.
3 Likes
Quick work! I gave it a try and noticed it was clobbering over caches with a path in the name (ie my cache is cache.example.org/main) so that didn’t work, here’s a small patch if you’d like:
patch:
diff --git a/app/Main.hs b/app/Main.hs
index a93ae4e..84ad846 100644
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -6,6 +6,7 @@ import Control.Lens ((%~), (<&>))
import Control.Monad (unless)
import Data.Attoparsec.ByteString.Char8 qualified as Atto
import Data.ByteString (ByteString)
+import Data.ByteString.Char8 qualified as B8
import Data.Foldable (foldlM)
import Data.Generics.Internal.VL ((^.))
import Data.Generics.Product (field, position)
@@ -98,9 +99,10 @@ determineCacheStatus manager request (NixCache nixCache) storePath = do
req =
request
{ Http.method = "HEAD"
- , Http.path = "/" <> T.encodeUtf8 hash <> ".narinfo"
+ , Http.path = basePath <> "/" <> T.encodeUtf8 hash <> ".narinfo"
, Http.responseTimeout = Http.responseTimeoutMicro 30_000_000
}
+ basePath = B8.dropWhileEnd (== '/') (Http.path request)
(hash, _, _) = storePathParseName storePath
determineCacheStatusConc
I agree w @withakay about runtime inputs, I would probably find this more useful if I could use --filter runtime or something to just check the runtime closure. I have 349 “misses” in my graph for one of my hosts but building doesn’t actually have any cache misses.
Also one more thing I’d like to see (np if it’s out of scope though) I’d love a small summary as output and meaningful exit codes, similar to nix-weather’s output. Ideally I could write the graph to a file and print a summary to stdout, with exit 1 if there’s anything missing from the cache, maybe as a configurable mode? Not exactly sure how that should look but you get the idea.
Thanks for the great work on this! Sorry for just making feature requests rather than just contributing directly, maybe if I can find some time I’ll contribute at some point.
1 Like
Any and all feature requests are welcome and id need all of the things you proposed myself. I want to put this in my CI
ideally logging these metrics into my telegraf+timescaledb+grafana stack so i can monitor cache percentage over time.
Will do this once I finish the hybrid/simple switch which is gonna solve your --filter runtime issue i think.
1 Like
@samiser okay, try with --mode simple, that requires you to provide a already built output path or it won’t work. But it does report 100% cache availability for my server’s configuration.
EDIT: I also applied your patch
The fix has a typo I think, it should just be one slash
After making that change locally it works great! Running it on the output showed 100% coverage. I also have a pretty specific use-case where running with --mode simple on the drv is actually useful, since in my CI i split eval from building via my cache and hand it off via the drv, so I can actually use that to check for drv + source cache coverage which is neat.
1 Like
@samiser what are you imagining as part of the report you described? it already prints [$cached/$total]. Not sure what exactly we could add.
ngl I literally just didn’t see it when I ran it the first time, but yea that’s what I meant lol
1 Like
ha, okay well that’s there. I’ll add a separate subcommand which only prints the report, perhaps also nicely formats the missing paths in markdown? something one could directly display in CI
sounds great! like you say i’d also love to use this in CI 
1 Like
So --mode simple on a derivation works for checking derivation availability? cool stuff
Also, i think the following is actually a bit better, i think it should avoid doubling slashes in all cases:
req =
request
{ Http.method = "HEAD"
, Http.path = basePath' <> T.encodeUtf8 hash <> ".narinfo"
, Http.responseTimeout = Http.responseTimeoutMicro 30_000_000
}
basePath = B8.dropWhileEnd (== '/') (Http.path request)
basePath' = if BS.null basePath then mempty else basePath <> "/"
(hash, _, _) = storePathParseName storePath
EDIT: please test, i dont have a cache in a sub-directory to test on
That does indeed work, I think basePath <> "/" is basically the same thing but your fix does work (though BS.null is a typo that i changed to B8.null locally)
I signed in and created a fork bc I was going to add some tests to verify but I figured you might have a preferred testing framework so I left it (since there currently aren’t any tests)
1 Like
Eh, I am not a big fan of tests
(kidding partly). I will write some one day. Feel free to write them if you want, though no LLMs please, I’m very strict about that.
EDIT: not a type, i just also imported Data.ByteString
1 Like
Fair enough, I’m a big fan of testing to avoid regressions but it’s your code base so I wouldn’t want to impose lol. And yea np on LLMs, I like doing things by hand anyway 
Ah that makes sense, I just followed what the compiler told me lol
1 Like
I mean, you’re free to do the tests, i dont have much experience. I wound have gone with hunit and tasty, i don’t think quickcheck makes too much sense here? maybe for the graph code?
If you’re itching to do something, you can implement a reverse adjacencyMap, it would allow us to fix the degree filter and optimize remove a bit.
Depends on what bit you’re testing, I come from OCaml so I’m a fan of expect-style tests (tasty-golden maybe) for things like graph/json output but for stuff like path handling you can just use plain unit tests. quickcheck makes less sense for that since there’s only really four interesting input shapes
And I may do that, I was just gonna add the tests as a quick thing but I’ll maybe look at doing that at some point if you haven’t picked it up already
1 Like