Fork me on GitHub
#fulcro
<
2023-01-25
>
genekim04:01:38

I’m wondering if anyone can help me understand a possible resolver performance issue. I often have resolvers load in an entire data set, say, 40K rows. This leads to a long first render time when loading a RAD report —say, 20 seconds. Never really thought much of it, because I thought it was database loading speed — I have such dependency/contempt of MySQL (which I run in Google Cloud SQL), I never really thought much of it. Thinking it was a database issue, I spent two embarrassing evenings trying to speed it — tried not using Cloud SQL Proxy, VPC connectors, etc. I even provisioned a new database, upgraded to MySQL 8.x, with lots of SSD, compute, RAM, but amazingly, it was still equally slow. Given all that work (and having to learn things I absolutely didn’t want to learn about), I was actually floored to find out that it wasn’t db load performance, but… My resolver. I found that my database loads were plenty fast, but my resolver was making the loads 3x slower. I finally learned how to use clj-async-profiler, and found something surprising. Here’s the flame graph —main humps are… 1. transforming the data from db: deserializing all those EDN strings, and converting instants to LocalDateTime or whatever. (30% of time) 2. this is the JDBC.next and MySQL loading (10% of time) 3. pathom3.format.eql (20% of time) 4. see second image — zoomed into the last two humps: two passes of rad.pathom-common/log! and/or rad.pathom-common/remove-omissions. (Combined: another 20% of time) Is there any way to speed the pathom or RAD parts up, maybe even skipping the #4 step? (For awhile, I thought I was generating errors in resolver [an EDN LocalDateTime causing errors], but even after removing that, no speedup.) (I disabled guardrails for this test.) Thank you, all! (@wilkerlucio, amazing to see all your work in performance — thanks for documenting it all!)

genekim04:01:19

IIRC, Pathom2 was about the same speed — but the majority of time was spent in elide-not-found.

genekim04:01:04

Well, wow... @UGBKV7HHP opened up this issue https://github.com/fulcrologic/fulcro-rad/issues/98, where @U0CKQ19AQ suggests eliminating (or turning into no-op) remove-emissions. Let's give that a try!

genekim06:01:41

Status update: created src/patch and made my own versions of pathom3.clj, and pathom-common.clj, and skipped the postwalk. I think it’s now 30% faster, as those two last peaks in flame graph are gone. Interestingly, GraalVM Java 19 makes it 20% faster than Azul Java 17! (Something about its optimizer does better on either EDN parsing or pathom3.eql transduce/reduce.) I’ll post source code next week. (My thanks to @U04V15CAJ for teaching me the trick of adding “src/paths” to :paths in deps.edn!)

genekim06:01:57

And thanks to @UGBKV7HHP for the hints!

roklenarcic07:01:11

Middlewares aren’t cheap. Middlewares that transform the result will take apart a huge persistent datastructure and put it back together. That is incredibly expensive.

👍 2
roklenarcic07:01:03

My main reservation with Pathom is the scenario you describe. EQL defining the data fetches as a tree is great in the abstract, but the reality is that we need to limit payload sizes, so paging is critical, then on top of that the tree transform Pathom does isn’t cheap, and worse, updating any single field in the tree is super expensive compared to updating a key in list of maps that SQL produces.

wilkerlucio22:01:37

hello @U6VPZS1EK! yes, indeed there is an overhead to Pathom that might get significant depending on the use case. Pathom is more optimal when dealing with complex data source, but with limited number of entities. also there is a significant difference if your main overhead is dealing with IO or if its computational. if its very IO driven, Pathom can do a good job on paralellizing, and given in those scenarios the IO time tend to dominate the cost, it ends up being a good tradeoff

wilkerlucio22:01:06

but when you have large collections that need to handle CPU intensive operations, then Pathom overhead might play a significant percentage of the total cost in the operation

wilkerlucio22:01:00

pagination is a good strategy, if that's something that fits your scenario, but sometimes it really doesn't, like when you need to make some reduction over a full list, delegating the process of each item also to the pathom process will have its cost

wilkerlucio22:01:40

IME in most cases we can avoid this scenario, but when we cant, there is a scape path in Pathom, that is to use ::pco/final true in the sequence meta. when you do that, Pathom will not process that value at all, and then you have to do it instead, if the process you need is simple enough in each item, that might just work fine

🤯 2
wilkerlucio22:01:10

but I'm always looking for ways to improve performance, if/when we can spot some specific situation to optimize that can handle some specific but common scenarios, those are great candidates

genekim04:01:05

Wow! This is fantastic , @wilkerlucio — I will try this out sometime next week. And I think Pathom is awesome — my posts were just documenting my discoveries, and I’m delighted I’ve learned more about how Pathom works. I’ll keep you posted! PS; I’ve done pagination, and am delighted by performance then. But I have use cases where it’s “quickly pull together a resolver / db query, and show it with minimal work” — which means not implementing paginated loads. I’m thinking of how I can make that easier with RAD. Catch y’all soon!

❤️ 2
genekim18:02:54

Looping back on this — @wilkerlucio, I confirmed that ::pc/final reduces by 2-3x the time required for pathom2 to return results. (And presumably, ::pco/final would do the same for pathom3.) I’ll be using this quite a bit for many of my projects when I do large loads. Thanks! (Code sample below for pathom2, just in case it’s helpful to people having the same problem of slow loads.)

(pc/defresolver all-tweets [env _]
       {; no ::pc/input
        ::pc/output [;:search-results/search-text
                     ;:search-results/matched
                     {:tweet/all-tweets
                      [:tweet/id
                       :tweet/tweet
                       :tweet/media
                       :tweet/text
                       :tweet/created-at-inst]}]}
       (let [
             ;search-text (-> env :ast :params :search/search-query)
             _           (log/warn "*** all-tweets: ")
             retval      (->> (queries/get-all-tweets)
                           vec)]
         (with-meta
           {:tweet/all-tweets retval}
           { ::p/final true})))))

wilkerlucio21:02:28

cool, one suggestion though, I would put the final in retval instead of the whole map, should have the same effect, to be honest I wonder how its working on the outside map, rsrs

🤯 2
genekim21:02:52

Thanks! Time to load is in same neighborhood, maybe even faster?

(pc/defresolver all-tweets [env _]
       {; no ::pc/input
        ::pc/output [;:search-results/search-text
                     ;:search-results/matched
                     {:tweet/all-tweets
                      [:tweet/id
                       :tweet/tweet
                       :tweet/media
                       :tweet/text
                       :tweet/created-at-inst]}]}
       (let [
             ;search-text (-> env :ast :params :search/search-query)
             _           (log/warn "*** all-tweets: ")
             retval      (->> (queries/get-all-tweets)
                           vec)]
         {:tweet/all-tweets
          (with-meta
            retval
            {::p/final true})}))))

genekim18:02:54
replied to a thread:I’m wondering if anyone can help me understand a possible resolver performance issue. I often have resolvers load in an entire data set, say, 40K rows. This leads to a long first render time when loading a RAD report —say, 20 seconds. Never really thought much of it, because I thought it was database loading speed — I have such dependency/contempt of MySQL (which I run in Google Cloud SQL), I never really thought much of it. Thinking it was a database issue, I spent two embarrassing evenings trying to speed it — tried not using Cloud SQL Proxy, VPC connectors, etc. I even provisioned a new database, upgraded to MySQL 8.x, with lots of SSD, compute, RAM, but amazingly, it was still equally slow. Given all that work (and having to learn things I absolutely didn’t want to learn about), I was actually floored to find out that it wasn’t db load performance, but… My resolver. I found that my database loads were plenty fast, but my resolver was making the loads 3x slower. I finally learned how to use `clj-async-profiler`, and found something surprising. Here’s the flame graph —main humps are… 1. transforming the data from db: deserializing all those EDN strings, and converting instants to LocalDateTime or whatever. (30% of time) 2. this is the JDBC.next and MySQL loading (10% of time) 3. pathom3.format.eql (20% of time) 4. see second image — zoomed into the last two humps: two passes of rad.pathom-common/log! and/or rad.pathom-common/remove-omissions. (Combined: another 20% of time) Is there any way to speed the pathom or RAD parts up, maybe even skipping the #4 step? (For awhile, I thought I was generating errors in resolver [an EDN LocalDateTime causing errors], but even after removing that, no speedup.) (I disabled guardrails for this test.) Thank you, all! (@wilkerlucio, amazing to see all your work in performance — thanks for documenting it all!)

Looping back on this — @wilkerlucio, I confirmed that ::pc/final reduces by 2-3x the time required for pathom2 to return results. (And presumably, ::pco/final would do the same for pathom3.) I’ll be using this quite a bit for many of my projects when I do large loads. Thanks! (Code sample below for pathom2, just in case it’s helpful to people having the same problem of slow loads.)

(pc/defresolver all-tweets [env _]
       {; no ::pc/input
        ::pc/output [;:search-results/search-text
                     ;:search-results/matched
                     {:tweet/all-tweets
                      [:tweet/id
                       :tweet/tweet
                       :tweet/media
                       :tweet/text
                       :tweet/created-at-inst]}]}
       (let [
             ;search-text (-> env :ast :params :search/search-query)
             _           (log/warn "*** all-tweets: ")
             retval      (->> (queries/get-all-tweets)
                           vec)]
         (with-meta
           {:tweet/all-tweets retval}
           { ::p/final true})))))