humbleui 2024-08-07

Forgot to post this on saturday - I spent a few hours trying to learn the humble primitives. Very nice! This is a spotify-ish ui. I did run into a couple questions along the way though: • First, performance. After around 200 elements paints were really costly. The main structure of the page is a stack with the top/bottom/list items, and the list is a column with two spacers at the top and bottom. I guess there isn't special handling for "offscreen-ness" in columns? I'm not sure. • Second, paint artifacts. You can see at the end of the video when I'm scrolling slowly that theres a "shimmer" at the top and bottom of the blurred areas. I'm not sure what's causing that? Seems to be related to a partially rendered text line. • Third (unfilmed), the top and bottom bars don’t capture the mouse. I see the hover effect when the mouse is on those bars. How can I turn those into a mouse sink?

Nevermind my assumption on #1, I looked at the virtualized rendering example again and I get the same behavior modifying that code - really long lists take a long time to render paint, even if only a small subset are actually drawn.

Do you have some repo for that?

Super nice stuff

I’d be eager to play around and investigate those performance issues

It's just a clone of the humble repo with an extra example. I'd be happy to publish it if you wanna mess around with it. I didn't (still don't) plan to take this shell to completion, so it's quite messy code 😂

Yeah, if you can publish, that’d be great. I’m also just messing around with Humble lastly so I’m just curious to check out what you’ve done :)

😎 1

Don’t really care if it’s messy, just hacking around checking what works and what doesn’t

Gimme ~2h

👍 1

1. Performance is what I’m working right now. There’s some virtualization (stuff not in viewport is not drawn, but still measured & laid out), can always have more. And there’s a terrible O² behavior in measure that I’m working on fixing 2. Blur like this requires some tricky compositing. I plan to explore that eventually but haven’t yet. There’s Blur demo in Humble repo, it has same problem 3.

👀 1

Not sure about 3, you want it to prevent hover from happening on stuff underneath?

Yeah, basically in a stack how do I get the higher element to sink mouse events

I’ve had something like this somewhere recently, I’ll look into it again

Any performance bottlenecks besides algorithmic?

Lots, but so far what I see if I cache layout properly (even go from O(n²) to O(n)) most times collapse to under a ms. Should be fine

But I do see stuff like map lookups, dynamic bindings and arithmetic ops on flame graph, which is a bit worrying. Hope they don’t add up to too much

Using dunamic bindings is very wasteful

There are some tricks you can pull of map lookups' cost becomes noticeable

I can look at the graphs if you'd like

Can you run Humble UI demo app? It’s just clone & ./script/repl.py Then select any demo, scroll left panel all the way down and click Profile button. After about ~10 seconds it should generate a flame graph and print path to it in console

👍 1

Oh, that’s how that’s supposed to work lol. That button crashes the app for me on Linux 🤣

It's been a while since I profiled it, I'll take a look later today

🙏 1

Lots of overhead from binding

you'll pay a lot less for it if you hand craft some macro which ends up emitting a call to RT.mapUniqueKeys(Object... init)

That's just two constructor calls which wrap the array and voila

Yeah, binding I plan to get rid of. I recently realized it’s all on the same thread, so can even do simple with-redefs

Also planning to extract scale to a separate top-level var, since 90% of use of ctx are for it, getting it through map lookup every time might not be the best idea

(defn evil-mapunique [& xs]
  (clojure.lang.RT/mapUniqueKeys (.array ^clojure.lang.ArraySeq xs)))

(defmacro evil-binding [bs & body]
  (assert (vector? bs))
  (assert (even? (count bs)))
  (assert (apply distinct? (take-nth 2 bs)))
  `(let []
     (push-thread-bindings (evil-mapunique ~@(mapcat (fn [[v e]] [`(var ~v) e]) (partition 2 bs))))
     (try
       ~@body
       (finally
         (pop-thread-bindings)))))

btw, profiling borked on linux because of

# "-Dclj-async-profiler.output-dir=/ws/humbleui",

Right! I was wondering where did I put it but couldn’t find it right away

Wonder if can be made relative

Relative or under /tmp

Also, checked with evil binding and eliminated all calls related to creating the hash map and iterating over the keys

/tmp is the default, but kinda hard to find

push-thread-bindings still has to merge maps though, so probably still overhead

I don't see the merge

I think it's a chained env

I didn't get increased throughput, trying to figure out why

Rough estimate of binding's overhead:

(def ^:dynamic *a*)
(def ^:dynamic *b*)
(def ^:dynamic *c*)

(defn foo1 [] (binding [*a* 1] 1))
(defn foo1' [] (evil-binding [*a* 1] 1))

(cc/quick-bench (foo1)) ; 204.706525 ns
(cc/quick-bench (foo1')) ; 84.057809 ns

(defn foo2 [] (binding [*a* 1 *b* 2] 1))
(defn foo2' [] (evil-binding [*a* 1 *b* 2] 1))

(cc/quick-bench (foo2)) ; 371.001624 ns
(cc/quick-bench (foo2')) ; 178.677062 ns

(defn foo3 [] (binding [*a* 1 *b* 2 *c* 3] 1))
(defn foo3' [] (evil-binding [*a* 1 *b* 2 *c* 3] 1))

(cc/quick-bench (foo3)) ; 531.225786 ns
(cc/quick-bench (foo3')) ; 242.553081 ns