Fork me on GitHub
#shadow-cljs
<
2024-03-07
>
agorgl10:03:58

Anybody using tailwindcss with shadow-cljs? I have troubles with tailwindcss watcher OOMing when shadow-cljs compiles its sources. My setup is this: In shadow-cljs.edn I have this:

{
 ...
 :builds
 {:app
  {:target     :browser
   :output-dir "resources/public/js/compiled"
   :asset-path "/js/compiled"
   :modules    {:app {:init-fn com.example.sample-project.core/init}}
   :devtools   {:repl-init-ns user
                :watch-dir "resources/public"}}}}
In package.json I have this:
{
  "name": "@com.example/sample-project",
  "scripts": {
    "watch": "run-p -l *:watch",
    "release": "run-s -l *:release",
    "shadow:watch": "npx shadow-cljs watch app",
    "shadow:release": "npx shadow-cljs release app",
    "tailwind:watch": "npx tailwindcss -i ./src/css/tailwind.css -o ./resources/public/css/compiled/site.css --watch",
    "tailwind:release": "NODE_ENV=production npx tailwindcss -i ./src/css/tailwind.css -o ./resources/public/css/compiled/site.css --minify",
  },
  ...
}
And in tailwind.config.js I have this:
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: process.env.NODE_ENV === "production"
    ? ["./resources/public/js/compiled/app.js"]
    : ["./src/**/*.{html,js,cljs}",
       "./resources/public/js/compiled/cljs-runtime/*.js"],
  theme: {
    extend: {},
  },
  plugins: [],
}
I run the whole thing with npm run watch The content in tailwind.config.js is inspired by this project: https://github.com/jacekschae/shadow-cljs-tailwindcss/blob/main/tailwind.config.js I'm not sure why we do have to track cljs-runtime folder when in development, but this is the thing that seems to trigger the oom in tailwindcss watcher process. Anybody that is casually using tailwindcss with shadow-cljs in their projects, what is your setup?

thheller10:03:00

looks fine to me

thheller10:03:31

need to track the cljs-runtime files in development since that contains the JS code tailwind can read

thheller10:03:57

since each namespace is a separate file. in release builds its all in one file.

thheller10:03:24

how does it oom? I mean the files aren't that large?

thheller10:03:51

you don't really need to track the sources, only if you also have CLJ server side code

agorgl10:03:31

Well, this happens even on the skeleton project

agorgl10:03:50

I have minimal cljs setup, but still

agorgl10:03:38

Let me check if I can make a very quick repro case for you

agorgl11:03:04

1. Have deps-new (https://github.com/seancorfield/deps-new) as a tool 2. Add an alias in your clojure.edn to use clojure 1.12 by adding :1.12 {:override-deps {org.clojure/clojure {:mvn/version "1.12.0-alpha7"}}} in your global aliases (just used for the template generation below) 3. Create a sample repo from using my template with clojure -A:1.12 -Tnew create :template io.github.agorgl/clj-fullstack%agorgl/clj-fullstack :name com.example.sample-poc 4. Change the pedestal.jetty dependency in deps.edn to version 0.7.0-SNAPSHOT (template fetches latest non-snapshots dynamically, but I've been using some stuff from the latest pedestal snapshot, this is required for build) 5. Run npm install to install package.json deps 6. Run npm run watch for the first time, let it finish 7. Stop npm run watch and run it again, now let it finish and wait a few seconds 8. Boom in the tailwindcss process (OOM)

agorgl11:03:45

By the way, why do we need to track cljs-runtime directory in addition to original src directories?

thheller13:03:51

Boom in the tailwindcss process (OOM)

thheller13:03:04

what does that mean exactly? what is the actual error?

thheller13:03:21

I mean how much memory does it consume when you run it without watch?

agorgl13:03:07

It prints a stack trace from node (tailwind) process I have a RAM widget and I can see it glide up 4Gigs in 3-4 seconds before the OOM stacktrace in node

agorgl13:03:42

I can repro and copy paste the stacktrace here really easily (so can you with the steps above if you want)

thheller13:03:33

can you create the repo for this? I can't actually help debug tailwind but I'm curious if it happens on my machine

agorgl13:03:58

Yes everything is set, just follow the steps above

agorgl13:03:26

It is not a repo, but it generates one from my template, its pretty easy to get it going

agorgl13:03:54

I can make a repo if you want to skip steps 1-4 though

agorgl17:03:16

@U05224H0W Here is a poc to make it easy for you: 1. git clone 2. npm install 3. npm run watch and wait till you reach [:app] Build completed. then Ctrl+C 4. npm run watch (again) and wait a few seconds after you reach [:app] Build completed. 5. You should see the oom crash on the tailwindcss node process, something in the lines of:

[shadow:watch  ] shadow-cljs - HTTP server available at 
[shadow:watch  ] shadow-cljs - server version: 2.27.5 running at 
[shadow:watch  ] shadow-cljs - nREPL server started on port 8777
[shadow:watch  ] shadow-cljs - watching build :app
[shadow:watch  ] [:app] Configuring build.
[shadow:watch  ] [:app] Compiling ...
[shadow:watch  ] [:app] Build completed. (172 files, 0 compiled, 0 warnings, 3.33s)
[tailwind:watch]
[tailwind:watch] <--- Last few GCs --->
[tailwind:watch]
[tailwind:watch] [18140:0x5fece4307220]    31011 ms: Mark-Compact (reduce) 2047.1 (2083.3) -> 2046.4 (2083.6) MB, 308.73 / 0.00 ms  (+ 2.9 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 326 ms) (average mu = 0.313, current mu = [18140:0x5fece4307220]    31502 ms: Mark-Compact (reduce) 2047.4 (2083.6) -> 2046.6 (2083.8) MB, 341.71 / 0.00 ms  (+ 0.7 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 360 ms) (average mu = 0.308, current mu =
[tailwind:watch]
[tailwind:watch] <--- JS stacktrace --->
[tailwind:watch]
[tailwind:watch] FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
[tailwind:watch] ----- Native stack trace -----
[tailwind:watch]
[tailwind:watch]  1: 0x5fece042b083  [node]
[tailwind:watch]  2: 0x5fece0830434 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
[tailwind:watch]  3: 0x5fece083082b v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
[tailwind:watch]  4: 0x5fece0a3c2dc  [node]
[tailwind:watch]  5: 0x5fece0a539d7 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
[tailwind:watch]  6: 0x5fece0a308d0 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
[tailwind:watch]  7: 0x5fece0a31919 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
[tailwind:watch]  8: 0x5fece0a136e3 v8::internal::Factory::NewFillerObject(int, v8::internal::AllocationAlignment, v8::internal::AllocationType, v8::internal::AllocationOrigin) [node]
[tailwind:watch]  9: 0x5fece0de728a v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [node]
[tailwind:watch] 10: 0x5fec811e53f6 
Also checkout how the memory increases in the last step

thheller18:03:12

apart from the duplicated namespace structure the repo looks fine

thheller18:03:29

cannot really help with debugging tailwind, dunno what it has issues with

thheller19:03:06

doesn't seem uncommon though

agorgl19:03:08

I'm starting to believe that the parallelism of the watchers maybe is the problem in this

agorgl19:03:40

Like the tailwind watcher could start working before the shadow-cljs compilation finishes

thheller19:03:40

how much actual memory do you have?

agorgl19:03:04

Yeah I could increase the node vm memory

agorgl19:03:39

You want to see if this happens with bigger available memory too?

thheller19:03:41

you can limit the memory shadow-cljs uses, but no clue about tailwind

agorgl19:03:54

I added NODE_OPTIONS="--max-old-space-size=8192" to tailwind:watch command and kinda circumvented the oom, although the behavior is still strange

agorgl19:03:27

A plain npx tailwindcss -i ./src/css/tailwind.css -o ./resources/public/css/compiled/site.css (without watcher) in another terminal after a shadow-cljs compilation takes 1 sec

agorgl19:03:03

Even with the node options above when in watcher mode it seems that shadow-cljs triggers the tailwindcss watcher multiple times and that is what causes the problem

agorgl19:03:22

I need some kind of debounce for this

thheller19:03:50

could be that tailwind is just a bit eager and triggers for every file watched?

agorgl19:03:04

I suppose it could be

thheller19:03:07

really no clue, I don't use tailwind anymore

agorgl19:03:24

Yeah I've seen your shadow-css I might switch to that at some point

agorgl19:03:38

I just needed compatibility with stuff like shadcn-ui for now 🙂

agorgl19:03:25

I'm starting to get somewhere with this: • I start tailwindcss watcher on one terminal using npx tailwindcss -i ./src/css/tailwind.css -o ./resources/public/css/compiled/site.css --watch, all good • I compile my cljs sources in another terminal using npx shadow-cljs compile app everything works well and fast. I repeat this multiple times, no problems appear • I start shadow-cljs watcher in the terminal I was doing the compile runs, with npx shadow-cljs watch app and I see that the tailwindcss watcher is rapidly consuming memory till the oom So my question is, does npx shadow-cljs watch app do anything different from npx shadow-cljs compile app command apart than watching the cljs sources? When starting npx shadow-cljs watch app shouldn't behave like calling npx shadow-cljs compile app just once (until some source changes?)

thheller19:03:26

watch injects more code, for hot-reload/REPL and stuff

thheller19:03:36

so compile is a bit more compact I guess

thheller19:03:52

watch also consumes more memory, since it holds the entire build state in memory longer than compile would

agorgl19:03:31

well my problem is not in the shadow-cljs watcher, but for some reason the js it produces makes tailwindcss watcher to blow up

thheller19:03:48

watch doesn't inject that much more code, so dunno why that would be a problem

agorgl19:03:34

I'll make a diff and I'll try to pinpoint this

thheller19:03:05

does it work if you run a watch, let it build and then shut it down? as in shadow-cljs shut down, not tailwind

agorgl20:03:02

Case 1: • Launch npx shadow-cljs watch app let it build, Ctrl + C • Launch npx tailwindcss -i ./src/css/tailwind.css -o ./resources/public/css/compiled/site.css --watch in another terminal, it works, no OOM Case 2: • Launch npx tailwindcss -i ./src/css/tailwind.css -o ./resources/public/css/compiled/site.css --watch leave it open • Launch npx shadow-cljs watch app in another terminal, let it build, wait a few seconds, tailwindcss watcher in the first terminal crashes with OOM

agorgl20:03:02

So I suppose according to this, the problem aren't the sources per se, but the way that shadow-cljs watch writes the files in the disk, triggering tailwind watcher multiple times or something?

thheller20:03:03

so its running OOM because shadow-cljs + tailwind consume too much memory together?

thheller20:03:41

so you could try to limit the JVM memory used, and as such make more available to node/tailwind

agorgl20:03:17

The OOM is not in the system, its JUST in the npx tailwindcss process

agorgl20:03:40

In the above examples, I use 2 terminals, one for each watcher

agorgl20:03:55

So I suppose with 2 npx invocations we get 2 node vms behind them

thheller20:03:59

I don't know what you mean. OOM is still OOM, doesn't really matter how you ran the things that consume memory

agorgl20:03:01

When I say OOM I'm refering to the crash that npx/node prints out to console and shuts down the command

agorgl20:03:14

E.g.

❯ npx tailwindcss -i ./src/css/tailwind.css -o ./resources/public/css/compiled/site.css --watch                                         49s

Rebuilding...

Done in 1099ms.

<--- Last few GCs --->
 =[108186:0x62f74ab9a220]   282162 ms: Mark-Compact (reduce) 2047.3 (2083.1) -> 2046.6 (2083.6) MB, 303.68 / 0.00 ms  (+ 3.2 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 326 ms) (average mu = 0.473, current mu =[108186:0x62f74ab9a220]   282694 ms: Mark-Compact (reduce) 2047.6 (2083.6) -> 2046.8 (2083.8) MB, 374.33 / 0.00 ms  (+ 6.2 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 402 ms) (average mu = 0.384, current mu =

<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----

 1: 0x62f747a2b083  [node]
 2: 0x62f747e30434 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
 3: 0x62f747e3082b v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
 4: 0x62f74803c2dc  [node]
 5: 0x62f7480539d7 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
 6: 0x62f7480308d0 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
 7: 0x62f748031919 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
 8: 0x62f7480136e3 v8::internal::Factory::NewFillerObject(int, v8::internal::AllocationAlignment, v8::internal::AllocationType, v8::internal::AllocationOrigin) [node]
 9: 0x62f7483e728a v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [node]
10: 0x62f6e87e53f6
zsh: IOT instruction (core dumped)  npx tailwindcss -i ./src/css/tailwind.css -o  --watch

agorgl20:03:36

My system still has lots of memory

agorgl20:03:48

node must limit the process to 4Gigs of memory or smth

thheller20:03:54

apparently it doesn't

thheller20:03:28

I mean how else do you explain this behavior? if you kill a shadow-cljs watch it doesn't touch the files or modify them in any way after being killed

thheller20:03:44

so they are the exact same, and you just confirmed that tailwind is fine when its dead?

thheller20:03:10

so the combination of both watches consumes too much memory

agorgl20:03:32

I think the problem is the watch trigger

thheller20:03:46

you can trigger it manually

agorgl20:03:58

If I build with shadow-cljs watch app and kill it, I have the files written before I start the tailwindcss watcher

thheller20:03:02

just touch one of the files or something without shadow-cljs running?

agorgl20:03:37

Yes but that does not replicate the shadow-cljs watch functionality, which I suppose touches all the files?

agorgl20:03:59

(at least when starting)

thheller20:03:12

well, when starting it writes all files yes

thheller20:03:21

on incremental updates it only touches the files that changed

agorgl20:03:26

so this is most probably the case

agorgl20:03:37

tailwindcss watcher watches all the files

agorgl20:03:46

shadow-cljs watcher writes all the files at startup

agorgl20:03:07

tailwindcss is being triggered multiple times in a few ms, and is not smart enough to 'debounce' the build

agorgl20:03:18

and tailwindcss process misbehaves leading to oom

thheller20:03:16

well yeah I guess if it triggers immediately for each file thats a problem

agorgl20:03:04

btw, just tried starting shadow-cljs watch app, let it build, kill it, start the tailwindcss watcher, touch a file from the output of shadow-cljs watch app and it does not have a problem

agorgl20:03:28

Another question I need to ask is: What files does shadow-cljs watches? I'm afraid that it could also be a cyclic dependency like shadow-cljs builds files -> tailwindcss gets trigger and builds css file -> shadow-cljs tries to build files again or smth?

thheller20:03:38

assuming that tailwind doesn't touch or generate any JS files, so it won't trigger a rebuild

thheller20:03:13

but in case you are on a mac there have been reports that icloud syncing can cause additional compiles. as in the project folder being synced to icloud

agorgl20:03:08

Nop, I'm on a pretty bare bones Linux laptop

agorgl20:03:12

No fancy stuff here

thheller20:03:21

well, then you can check if something funky is happening in the shadow-cljs terminal log. it'll log when it compiles something, so you should be able to identify recompile loops

agorgl20:03:48

the one on stdout?

agorgl20:03:24

Can I somehow enable verbose logging to print a line for each file built in shadow-cljs?

thheller20:03:34

[:app] Build completed. (172 files, 0 compiled, 0 warnings, 3.33s)

agorgl20:03:37

I want to see the rate that it writes files on startup

agorgl20:03:49

Yeah no build loops seem to happen

thheller20:03:14

:log {:level :debug} in shadow-cljs.edn top level logs a bit more

thheller20:03:34

npx shadow-cljs watch app --verbose logs more compile progress

agorgl20:03:41

I suppose the write operations are the flush ones?

thheller20:03:19

I guess you could try skipping the cljs-runtime dir and only run over the sources

thheller20:03:39

but in the past tailwind didn't recognize some cljs code properly and didn't find many aliases

agorgl20:03:13

Yeah that is my main question now, why do I even watch cljs-runtime dir

agorgl20:03:08

I'm using tailwindcss with hiccup and :class "<tw space separated classnames here>"

agorgl20:03:17

Is there a specific case it won't work?

thheller20:03:26

its been years, can't really remember

thheller20:03:46

class strings should be fine

agorgl20:03:51

The jackshae repo that comes first in google does not have an explanation either

thheller20:03:10

an argument for running over the output files is that it can also find tailwind uses in libraries, but I guess those aren't very common

thheller20:03:47

for release builds I'd definitely run over just the output, but for dev I guess it doesn't matter too much

geraldodev21:03:12

Hi @U03PYN9FG77 I'm using tailwind_watcher.clj which is a stripped down version of the one you are using. Basically I'm not using the release bits. I can confirm that I had a Javascript out of memory once. I don't know how to repeat but i suspect I've clicked force compile in a already watched project.

geraldodev21:03:04

I'm using this conf in my shadow-cljs.edn app build to pass parameters to the watcher, and it to be invoked more integrated with the cljs compilation.

:tailwindcss {:input "./styles/globals.css"
              :config "./tailwind.config.js"
              :output "./resources/public/css/app.css" }
:build-hooks [(tailwind-watcher/start-watch!)
It depends on babashka.process which I've included as :dev dependency on deps.edn
babashka/process {:mvn/version "0.5.22"} ;; Necessary to invoke tailwindcss
I've placed the tailwind_watcher.clj inside /dev directory (right along with user.clj) so it just take effect in dev mode.

agorgl08:03:45

An update for anybody that stumbles upon this in the future: I replaced the tailwindcss watcher with the more generic watchexec that supports debounce and seems way more stable. Now my package.json is:

{
  "name": "@com.example/clj-fullstack-sample",
  "scripts": {
    "watch": "run-p -l *:watch",
    "release": "run-s -l *:release",
    "shadow:watch": "npx shadow-cljs watch app",
    "shadow:release": "npx shadow-cljs release app",
    "tailwind:watch": "node -p \"require('./tailwind.config').content.map(x => x.replace(/^\\.\\//, '')).join('\\\\n')\" > .twpath; RUST_LOG= WATCHEXEC_FILTER_FILES=.twpath watchexec -q -d 1000 --no-vcs-ignore -- npx tailwindcss -i ./src/css/tailwind.css -o ./resources/public/css/compiled/site.css",
    "tailwind:release": "NODE_ENV=production npx tailwindcss -i ./src/css/tailwind.css -o ./resources/public/css/compiled/site.css --minify",
    "babel:watch": "npx babel src/js -d gen/js -x \".js,.jsx,.es6,.es,.mjs,.cjs,.ts,.tsx\" --watch",
    "babel:release": "npx babel src/js -d gen/js -x \".js,.jsx,.es6,.es,.mjs,.cjs,.ts,.tsx\""
  },
...
}

agorgl08:03:07

I use node to extract the content paths from the tailwind.config.js, write them to a file and pass that to watchexec to avoid duplication of the watch paths. Then watchexec uses debounce time of 1 sec to call the regular npx tailwindcss build command to run the tailwindcss build every time a relevant file changes

thheller08:03:47

still don't understand how it runs out of memory? seems like memory use should be constant if it just runs over the same sources multiple times?

thheller08:03:10

hmm I guess maybe it triggers while its still building the previous run?

agorgl08:03:14

@U05224H0W the problem is not the compile process but the watcher

agorgl08:03:17

yes exactly

agorgl08:03:51

it does not have any debounce mechanism so I believe the tailwindcss watcher triggers multiple times in a short timespan and this leads to this behavior