This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2019-07-26
Channels
- # aleph (9)
- # announcements (10)
- # aws (1)
- # beginners (65)
- # calva (9)
- # cider (11)
- # clj-kondo (1)
- # cljdoc (61)
- # cljsrn (6)
- # clojars (2)
- # clojure (40)
- # clojure-austin (1)
- # clojure-belgium (1)
- # clojure-europe (4)
- # clojure-finland (1)
- # clojure-france (1)
- # clojure-italy (57)
- # clojure-nl (6)
- # clojure-spec (134)
- # clojure-uk (67)
- # clojuredesign-podcast (2)
- # clojurescript (40)
- # cursive (25)
- # datascript (1)
- # datomic (8)
- # events (1)
- # figwheel-main (18)
- # fulcro (36)
- # immutant (5)
- # jobs (5)
- # joker (3)
- # kaocha (41)
- # leiningen (4)
- # luminus (4)
- # off-topic (13)
- # onyx (8)
- # pedestal (2)
- # perun (7)
- # planck (2)
- # protorepl (9)
- # re-frame (3)
- # reagent (73)
- # reitit (5)
- # shadow-cljs (186)
- # sql (4)
- # vim (1)
- # yada (2)
Any good tools for debugging slow generators? We have lots of specs with tens of s/and
preds & custom gens associated. I'd like to know which predicate is causing a such-that to fail often.
I bet you could alter-var-root such-that
to default to a lower max-tries
looks like spec just uses the default (10) as far as I can tell
so if you change that to, e.g., 2, you might get more failures
but maybe that's not useful if the "telling you which s/and
failed" feature isn't wired up yet
Good idea on the such-that max-tries thing. That should help a bit. Though, the such-that errors are usually pretty obtuse, hiding the actual name of the predicate behind an anonymously named test.check pred.
spec's roadmap, yes, as far as I know; that feature couldn't exist prior to the 0.10.0 versions of test.check
> the such-that errors are usually pretty obtuse that's exactly the problem that the aforementioned up-wiring would address
Ah, very cool. Debugging these issues has been a huge time sink for us. It seems like it should be a fairly easy change to spec. What feature in 0.10.0 allows this better up-wiring?
from the such-that
docstring:
You can customize `such-that` by passing an optional third argument, which can
either be an integer representing the maximum number of times test.check
will try to generate a value matching the predicate, or a map:
:max-tries positive integer, the maximum number of tries (default 10)
:ex-fn a function of one arg that will be called if test.check cannot
generate a matching value; it will be passed a map with `:gen`,
`:pred`, and `:max-tries` and should return an exception
I have no idea, @U064X3EF3 probably knows at least the easy vs hard part
probably not easy?
well, not sure
I'd think you'd want have some kind of path on hand at the point where such-that
is called
so might take some wiring to make that path available
or maybe it's already there, since spec likes to put paths in its error messages
This would be a huge value add for us. If adding/hacking this in is a day's worth of work, it's definitely worth it for us to dive in
it should be - gens have the path
but they aren't using such-that right now
so I'm not sure how this stuff connects
they? this is about s/and
in particular I think
I can't imagine the gen for s/and
doesn't use such-that
I guess it does, just not directly
that's generically in gensub, which handles the recursion check stuff
It appears there's only one place that such-that
is used in spec, gensub as you say. It also is passed other info that seems valuable to report back.
are you using deps.edn ?
if so I could commit something on a branch and you could test it as a git dep
I currently don't have a repro of one of these such-that issues though, just the slow gen thing. The such-that errors are very frequent though
easy to repro with any restrictive s/and pred
my first hack did not yield anything useful though
I need to step away for a bit, but may play with it later
@kenny 95% of the time the issue with slow generators is generating large (and particularly nested large) collections
setting :gen-max 3
in s/coll-of
, s/map-of
etc is often a big difference
This structure is highly nested & has lots of colls. We have these opts set:
:coll-check-limit 10
:coll-error-limit 10
:fspec-iterations 10
:recursion-limit 1
none of those affect the gen'ed count
with a 3 level coll, you could have 10x10x10 nested elements
well, recursion limit might affect the depth (although I think there are some scenarios where it is not getting applied right now)
if you have fspecs, that's another possible issue where you might want to consider simpler preds
like ifn?
We have considered that a number of times simply due to the massive amount of time debugging generators take. Every time, however, we always decide not to because those fspec generators catch valuable bugs haha
another testing tip is that gens will get run 1000 times in check
and size/complexity increases
so gen/sample
by default (20) will not expose generator issues
but you can gen/sample
with 1000 and will see those
if you separate the args spec for an fdef into its own spec, it's easy to test that
(s/fdef foo :args ::args-spec)
, then (gen/sample (s/gen ::args-spec) 1000)
I searched for :gen-max
in this service and didn't find any so it may not be used. Though, there are tons of custom gens which use the gen/*
functions which don't take in :gen-max
, I think.
So that's the usual way to debug the slow gens -- something like this (do (doall (gen/sample (s/gen ::db/db-control-args) 1000)) nil)
. Then that will hang forever and that's where the pain starts.
I would try just blindly adding :gen-max 3
to all the collection specs
@kenny what version of test.check are you using?
there is a ticket specifically for changing this default (or exposing a global default) btw
which I am completely failing to find
but I wrote the patch for it, so I know it exists
there's actually a bug report about that
so be careful on that
Yeah, that would help to figure out if the problem is collection gen or something else.
okay, that should avoid some exacerbatory issues with the 0.9.0 release
oh good
Another thing that slows down debugging slow gens is that the check
tests still run in the background even though I interrupted the current op in the REPL.
is that a pmap
thing?
I think spec does
but could be wrong
Getting a frequencies
map returned after generating a spec with s/and
would also be super helpful. The keys would be the forms of each s/and pred.
well shutdown-agents will shutdown the pool never to restart again, so you'd still need to bounce your repl
pmap is lazily parallel
so when you stop using the result, up to n-1 (where n is number of processors) may still be working to compute the next few results in the lazy seq
Hmm. It seems to go on forever. I’ll add prints to some functions to debug the slow gens and it never stops printing after interrupting.
Perhaps because of the doall
in this line? (do (doall (gen/sample (s/gen ::db/db-control-args) 100)) nil)
This is helpful :
(let [such-that gen/such-that]
(with-redefs [gen/such-that (fn
([pred gen]
(such-that pred gen 1))
([pred gen opts]
(such-that pred gen 1)))]
(gen/generate (s/gen ::db-control-args))))
Actually, @gfredericks the problem with taking the above approach is simple generators sometimes need the such-that. e.g.
(s/def ::int+-interval
(s/and (s/tuple ::m/int+ ::m/int+)
(fn [[x1 x2]] (>= x2 x1))))
there already is an interval spec with a generator - s/int-in
ah, I see
@kenny yeah the general rule with such-that
is that the predicate needs to be highly-likely to succeed; so for something like an interval you'd want to write your own generator, which is pretty easy
isn't it just 50%?
more precisely what you want is "astronomically unlikely to fail 10 times in a row"
and something that only passes 50% of the time doesn't have that quality
as such-that
retries, it's also incrementing size
, so often it's good enough that the probability of failure goes down with larger size
e.g., gen/not-empty
relies on that
It almost seems like you should be able to enable a warning that tells you when your specs use s/and and don't have a custom gen.
Writing the generator for an interval, like the above is tricky without such-that because you don't know how to generate a value that will match the spec. I was originally thinking it'd just be a gen/bind
but that doesn't work because I don't know how to generate a ::m/int+
such that the second one is greater than the first without using such-that.
the trick is to generate a starting value and the range size, then gen/fmap the pair from that
one option: before doing a such-that add something like this https://github.com/bfabry/specify-it/blob/master/src/specify_it/bst_spec.clj#L268 that just reports on how often the such-that would succeed. then tune your inputs until it’s a reasonable frequency
Well this is a reasonable frequency but it's certainly not "astronomically unlikely to fail 10 times in a row"
in the case that you’re going to throw away generations that don’t match your predicate, I would put “reasonable frequency” at like 50%
That seems right. This would go back to the original problem then where, in practice, overriding such-that doesn't really work as a debugging technique.
a test.check generator mode where all of the various predicates print out their form line number and % success rate at the end would be cool (and quite hard to do)
@kenny w.r.t. avoiding such-that, how about fmap with sort?
fmap can be used to avoid such-that in a lot of cases another example is here: https://clojure.org/guides/test_check_beginner#_generator_combinators
@gfredericks That cleaned up this code nicely 🙂 Thanks for the idea! https://github.com/Provisdom/math/blob/488573a16947eab78ecd096c7ef71a33f94cd0d7/src/provisdom/math/intervals.clj#L14-L55
I would generate the starting value and the range size, then fmap to get [start (+ start size)]
For example, we have a "prob-interval" where the interval's lower and upper bounds must be between 0 and 1.
One issue with @gfredericks approach, however, is if you have a strict-interval. You have a chance of hitting the such-that then.
Sort+inc?
You could add Double/MIN_VALUE in that case. But if the interval is integers only, that won't work.
Math/nextUp 🙂
Might be an oversight, might be a reason it won’t work with dynaload
Won't work because lazy-combinator
isn't qualified 😞 https://github.com/clojure/spec.alpha/blob/5228bb75fa10b7b13ea088d84f4d031cce74a969/src/main/clojure/clojure/spec/gen/alpha.clj#L92
I need something like (s/tuple (s/tuple ::a) (s/tuple ::b))
, but should allow values like [["a" "d"] ["b"]]
(ignore "extra" tuple values)
(s/cat :a (s/spec (s/cat :a ::a
:extra (s/* any?)))
:b (s/spec (s/cat :b ::b
:extra (s/* any?))))
^ this one works, but it's really ugly(s/def ::tup1 (s/tuple string?))
(s/def ::tup2 (s/tuple string? string?))
(s/def ::tup (s/or :tup1 ::tup1 :tup2 ::tup2))
(s/tuple (s/tuple ::tup ::tup))