nbb

Chris McCormick 2025-06-20T11:44:09.884039Z

cljs-shrinkwrap bundles up your nbb script, plus nbb, plus deps from node_modules into a single "binary" which only depends on Node.js:

npm i cljs-shrinkwrap
npx shrinkwrap myscript.cljs myscript
This will produce an executable script in ./myscript that only needs Node to run. https://github.com/chr15m/cljs-shrinkwrap I'm not sure if this deserves to be a utility since it's basically just following the steps from the nbb wiki. One thing I could do is fold this into nbb itself, similar to nbb bundle - something like nbb shrinkwrap which would depend on having @vercel/ncc installed. 🤔 Would that make more sense than a standalone utility?

borkdude 2025-06-20T12:29:01.959129Z

how is ncc different from esbuild etc?

Chris McCormick 2025-06-21T02:26:46.357259Z

Hm good question. I think I tried esbuild and found the configuration too complicated whereas ncc is one command to do the exact thing I wanted. That said it seems like ncc might have issues supporting esm.

Chris McCormick 2025-06-21T04:17:47.357919Z

I replaced ncc with esbuild and it works well, however when bundling a small script that was working with ncc I am now getting an error. Working through it.

Chris McCormick 2025-06-21T05:41:53.317909Z

Ok the error was to do with a dynamic require of node builtin "events" not being handled/translated by esbuild. The LLM gods suggested: :banner {:js "import { createRequire } from 'module';const require = createRequire(import.meta.url);"} And this magical incantation fixes the issue. 🕉️

Chris McCormick 2025-06-21T05:59:27.785839Z

So if we added a nbb shrinkwrap command it could be dependent on esbuild instead of ncc. Instead of making esbuild a requirement we could check for it at runtime and dynamically import it and print a useful error if it isn't present.

(p/let [esbuild-result (esbuild/build
                             (clj->js {:entryPoints [(fs/realpathSync mjs-file)]
                                       :bundle true
                                       :platform "node"
                                       :minify true
                                       :format "esm"
                                       :banner {:js "import { createRequire } from 'module';const require = createRequire(import.meta.url);"}
                                       :write false}))
            output-file-obj (first (.-outputFiles esbuild-result))
            bundled-code (.-contents output-file-obj)]
      (println (str "esbuild bundling complete."))

      ;; Step 3: Create executable binary
      (fs/writeFileSync output-file
                         "#!/usr/bin/env -S node --experimental-default-type=module\n")
      (fs/appendFileSync output-file bundled-code)
      (run-command (str "chmod 755 " output-file)))

borkdude 2025-06-20T12:27:49.182879Z

I'm curious how long this takes to run you y'alls systems:

$ npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
10000000
"Elapsed time: 387.168708 msecs"

2025-06-20T13:08:44.663479Z

500.78ms on my MacBook Air M2, 365.87ms on my MacBook Pro M4 Max

borkdude 2025-06-20T13:09:29.150669Z

gotcha, M4 Pro here as well

teodorlu 2025-06-20T13:12:35.737399Z

$ npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
10000000
"Elapsed time: 384.984667 msecs"
M4 pro here too 😄

borkdude 2025-06-20T13:14:52.583229Z

and bb?

$ bb -e '(time (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j)))))'
"Elapsed time: 353.564958 msecs"
10000000
Similar

teodorlu 2025-06-20T13:19:59.755229Z

$ bb -e '(time (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j)))))'
"Elapsed time: 350.56525 msecs"
10000000
$ bb version
babashka v1.12.200
Similar indeed! (you made me realize I hadn't updated brew packages in a long while)

2025-06-20T14:28:49.634669Z

nbb

"Elapsed time: 527.623750 msecs"
Only a slow M2 here

2025-06-20T14:30:14.263119Z

bb version && bb -e '(time (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j)))))'
babashka v1.12.201
"Elapsed time: 495.332 msecs"
10000000

Chris McCormick 2025-06-21T03:56:31.096429Z

$ npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
10000000
"Elapsed time: 511.161188 msecs"
Dell XPS 13 9305 (from 2021)

jasalt 2025-06-27T06:59:44.318119Z

Tested on few laptops, also de-dusted trusty old Thinkpad X220. Millisecond timings with rough average over few runs. Kernel parameter mitigations=off on all but latest CPU.

| CPU                         | nm | year |  nbb |   bb |
|-----------------------------+----+------+------+------|
| AMD Ryzen 7 PRO 7840U       |  4 | 2023 |  440 |  454 |
| AMD Ryzen 7 PRO 4750U       |  7 | 2020 |  638 |  745 |
| Intel(R) Core(TM) i7-1165G7 | 10 | 2020 |  540 |  565 |
| Intel(R) Core(TM) i5-8350U  | 14 | 2017 | 1100 | 1010 |
| Intel(R) Core(TM) i7-2620M  | 32 | 2011 | 1450 | 1400 |
Seems nbb runs slightly faster on AMD's, especially on that 4750U (~10%) . Opposite effect on that i5-8350U which era still seems popular on some cheap VPSs'. Used benchmark commands:
bb -e '(time (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j)))))'
npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
- babashka-1.12.203-linux-amd64 - nodejs v18.19.0 / nbb@latest 1.3.204

🙏 1
jasalt 2025-06-27T07:42:51.904869Z

Weirdly on one machine I get

npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))' 
npm ERR! cb.apply is not a function
npm ERR! A complete log of this run can be found in:
npm ERR!     /home/user/.npm/_logs/2025-06-27T07_41_27_916Z-debug.log
cat /home/user/.npm/_logs/2025-06-27T07_41_27_916Z-debug.log
0 info it worked if it ends with ok
1 verbose cli [
1 verbose cli   '/usr/bin/node',
1 verbose cli   '/usr/local/lib/node_modules/npx/node_modules/npm/bin/npm-cli.js',
1 verbose cli   'install',
1 verbose cli   'nbb@latest',
1 verbose cli   '--global',
1 verbose cli   '--prefix',
1 verbose cli   '/home/user/.npm/_npx/5839',
1 verbose cli   '--loglevel',
1 verbose cli   'error',
1 verbose cli   '--json'
1 verbose cli ]
2 info using npm@5.1.0
3 info using node@v18.19.0
4 verbose npm-session 16b378774e1841ee
5 silly install loadCurrentTree
6 silly install readGlobalPackageData
7 http fetch GET 200  16ms (from cache)
8 silly pacote tag manifest for nbb@latest fetched in 29ms
9 verbose stack TypeError: cb.apply is not a function
9 verbose stack     at /usr/local/lib/node_modules/npx/node_modules/npm/node_modules/graceful-fs/polyfills.js:287:18
9 verbose stack     at FSReqCallback.oncomplete (node:fs:203:5)
10 verbose cwd /home/user/sites
11 verbose Linux 6.12.13-amd64
12 verbose argv "/usr/bin/node" "/usr/local/lib/node_modules/npx/node_modules/npm/bin/npm-cli.js" "install" "nbb@latest" "--global" "--prefix" "/home/user/.npm/_npx/5839" "--loglevel" "error" "--json"
13 verbose node v18.19.0
14 verbose npm  v5.1.0
15 error cb.apply is not a function
16 verbose exit [ 1, true ]

jasalt 2025-06-27T07:43:23.777449Z

On debian 12, purged the nodejs installation but still persists.

jasalt 2025-06-27T07:47:13.010649Z

..but there it works still without having the @latest set in the command, just npx nbb -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))' , it seems to point to same nbb version though

jasalt 2025-06-27T07:55:51.705889Z

Anyways, Moore's Law RIP. Today this should run in 10ms if worked still, looking at the speed on 2011's CPU.

👍 1
borkdude 2025-06-27T17:37:14.134359Z

@j.s which node version?

jasalt 2025-06-27T18:37:54.015869Z

@borkdude nodejs v18.19.0 there also. Seems like something has to be gone bit out of place. I don't mind really as otherwise things seemingly work and reinstalling with next OS update soon anyways. Can send more diagnostics if it smells like a bug that should be investigated.

borkdude 2025-06-27T18:39:11.137779Z

ok, if it persists on the new OS let me know

jasalt 2025-06-27T18:48:34.835049Z

yea, sure. that'd be pretty weird if it did

jasalt 2025-06-27T20:36:02.163899Z

Benchmarking Janet on machine where I got 440ms with nbb and 454ms with bb, running similar program. With it I'm getting 1165ms with recursive function and 762ms when mutating value in loop. Read somewhere Janet would be around same ballpark to CPython but didn't expect it to be this much slower vs. babashka, but well..

jasalt 2025-06-27T20:36:20.714989Z

# Time macro equivalent from 
(defmacro timeit [& body]
    # generate unique symbols to use in the macro so they can't conflict with anything used in `body`
    (with-syms [$t0 $t1]
        ~(do
            (def $t0 (os/clock :monotonic :double))
            (do ,;body)
            (def $t1 (os/clock :monotonic :double))
            (- $t1 $t0))))

# Use recursive function call as there's tail call optimization
(defn count-recurse [i j]
  (if (zero? j)
    i
    (count-recurse (inc i) (dec j))))

(printf "Took %.3f seconds recursing" (timeit (count-recurse 0 10000000)))

# Mutating
(defn count-loop []
	(var i 0)
	(var j 10000000)
	(while (not (zero? j))
	  (++ i)
	  (-- j))
	i)

(printf "Took %.3f seconds mutating" (timeit (count-loop)))


## -> Took 1.165 seconds recursing
## -> Took 0.762 seconds mutating

borkdude 2025-06-27T20:38:16.999279Z

bb loops are also faster than python :)

jasalt 2025-06-27T20:38:30.616919Z

Yup, I remember reading that somewhere

borkdude 2025-06-27T20:38:35.266879Z

(the normal Python, GraalPython is faster)

jasalt 2025-06-27T20:38:49.489369Z

right

jasalt 2025-06-27T20:42:21.716799Z

Janet's lower memory use with basic http server is compelling though, I did get around 15MB (idle memory usage) there while bb http-kit and nbb express were around 50MB, though I don't know if there's room to tune those.

borkdude 2025-06-27T20:42:40.998579Z

you can set the memory with -Xmx etc args

jasalt 2025-06-27T20:43:42.142909Z

ok, i'll check those

borkdude 2025-06-27T20:56:55.811219Z

bb -e '(org.httpkit.server/run-server prn {:port 8889}) @(promise)'
This runs with taking 15mb memory on my system

souenzzo 2025-06-23T10:52:53.495989Z

mac m1 pro clj (jvm): ~15msecs cljs (node): ~7msecs nbb (1.3.204): ~500msecs

jasalt 2025-06-28T07:50:17.661709Z

Okay, I'll check how that compares. I hastily measured with https://github.com/babashka/http-server earlier running bb ./http-server --port 8888 --dir . and just the Janet joy example. Running single bb process globally, memory use in MiB, inspected at KDE System Monitor:

| server           | memory | shared | note                      |
|------------------+--------+--------+---------------------------|
| janet (circlet)  |    2.3 |    2.8 |                           |
| janet (joy)      |   11.4 |    3.1 | 2 processes, 5.7 Mib each |
| bb (http-kit)    |   31.4 |   29.4 |                           |
| bb (http-kit) 3x |  *16.5 |  *30.9 |                           |
*avg for each of 3 processes
Noted that running bb -e '(org.httpkit.server/run-server prn {:port 8839}) @(promise)' three times parallel shows lower per-process memory use. I couldn't get Janet networking docs basic net/listen example working but joy framework I guess builds on it, used: - https://github.com/janet-lang/circlet (c mongoose server) - https://github.com/joy-framework/joy Maybe tuning the bb setup could improve it a bit, while it's bit of like comparing apples to oranges.

jasalt 2025-06-28T07:51:42.128299Z

Also quick Python comparison: - Flask ASGI (uvicorn) 29.0MiB / 12.4Mib (shared). - Nanodjango with ASGI and 1 WSGI worker is around 51.6MiB (12.8MiB per worker). There was some Python running in containerd process already which might share few megabytes of RAM for those.

borkdude 2025-06-28T07:51:48.539789Z

you could try adding -Xmx10m or so but on my machine it didn't have any effect I think

borkdude 2025-06-28T07:52:31.409369Z

-Xmx should go as the first argument

jasalt 2025-06-28T07:57:19.028869Z

I don't think it's effective, tried bb -Xmx10M -e '(org.httpkit...

borkdude 2025-06-28T07:57:37.026809Z

I think it has to be small m

borkdude 2025-06-28T07:57:45.537969Z

but even then I also didn't see an effect

borkdude 2025-06-28T07:58:03.151219Z

if you misspell the letter, it will complain though

jasalt 2025-06-28T07:58:03.278119Z

Yea, bb -h states capital though

borkdude 2025-06-28T07:58:26.905309Z

interesting, maybe both are valid

jasalt 2025-06-28T07:58:27.031259Z

no complaints

jasalt 2025-06-28T07:58:56.619869Z

could there be a hard minimum limit or something, i'm not familiar on stuff that deep

borkdude 2025-06-28T07:59:09.992599Z

could be

borkdude 2025-06-28T08:01:01.124299Z

$ rlwrap bb -Xms0m -Xmx0m
Babashka v1.12.202 REPL.
Use :repl/quit or :repl/exit to quit the REPL.
Clojure rocks, Bash reaches.

user=>  (-> (Runtime/getRuntime) (.maxMemory))
13743685632
user=> (/ 13743685632 (* 1024 1024))
13107

borkdude 2025-06-28T08:01:18.791259Z

without those args it's the same though

borkdude 2025-06-28T08:02:04.625949Z

ah:

$ rlwrap bb -Xmx100m
Babashka v1.12.202 REPL.
Use :repl/quit or :repl/exit to quit the REPL.
Clojure rocks, Bash reaches.

user=>  (-> (Runtime/getRuntime) (.maxMemory))
104857600
user=>  (-> (Runtime/getRuntime) (.maxMemory) (/ (* 1024 1024)))
100

borkdude 2025-06-28T08:02:58.178299Z

$ bb -Xmx50m -e '(-> (Runtime/getRuntime) (.maxMemory) (/ (* 1024 1024)))'
50

borkdude 2025-06-28T08:03:28.347959Z

I think 16 is the mimimum. 25 still works, but 10 gives 16

jasalt 2025-06-28T08:03:46.827729Z

Yea, seems so

jasalt 2025-06-28T08:07:38.832049Z

Might not be correct way to try optimize server idle memory use this way (restricting heap size), if it's okay that processing takes whatever it takes but minimum memory should be required when listening requests.

jasalt 2025-06-28T08:12:18.813679Z

But practically I think it's pretty good already. Wouldn't choose Janet only for this sort of memory savings in most cases.

jasalt 2025-06-28T08:50:19.817589Z

Though it does seem tempting on a machine running out of memory already to have something that barely shows up on system monitor without scrolling all way to bottom.

jasalt 2025-06-28T08:51:30.556819Z

Managed to get this Janet server code responding, it takes 2MiB (2.6MiB shared):

(defn http-handler
  "Handle HTTP connections and return Hello World"
  [stream]
  (defer (:close stream)
    # Read the HTTP request (we'll ignore it for this simple example)
    (def request-buffer @"")
    (:read stream 1024 request-buffer)

    # Send HTTP response with Hello World
    (def response "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\n\r\nHello World!")
    (:write stream response)))

# Create and start the HTTP server
(def server (net/listen "127.0.0.1" "8080"))
(forever
  (if-let [conn (:accept server)]
    (ev/call http-handler conn)))
net/listen sources https://github.com/janet-lang/janet/blob/f2eaa5de/src/core/net.c#L653

borkdude 2025-06-28T08:52:03.392259Z

impressive