bcardiff/crystal-fswatch

BUG: Process hang on M1

Opened this issue · 9 comments

This code will cause the process to go unresponsive and hang on an M1:

require "fswatch"

FSWatch.watch "." do |event|
  puts "got event for #{event.path}"
end

I'm not sure if this is also an issue on other systems. I have fswatch installed from brew, and I didn't see anything in their issues which might indicate that it's related:

> brew info fswatch
==> fswatch: stable 1.17.1 (bottled)

FSWatch.watch will not block the main fiber. I think the program is finishing right away.

Validated with

% crystal --version
Crystal 1.11.2 (2024-01-18)

LLVM: 17.0.6
Default target: aarch64-apple-darwin23.2.0

% brew info fswatch
==> fswatch: stable 1.17.1 (bottled)

macOS 14.2.1 (23C71)

FSWatch.watch will not block the main fiber. I think the program is finishing right away.

I'm also able to trip into the scenario while using Kemal to block the main fiber:

require "kemal"
require "fswatch"

FSWatch.watch "." do |event|
  puts "got event for #{event.path}"
end

get "/" do |env|
  "Hello World!"
end

Kemal.run

When I run watch -n 1 curl http://localhost:3000 I can see kemal responding to requests until a single FSWatch event fires, and then it gets blocked. This is how I ran into the bug myself, I should have posted the full context before.

Single thread or multi thread?

I'll try to repro tomorrow

Just single thread. I'm not doing anything fancy with mt. Thanks for looking.

It seems that there is an issue with crystal 1.11.x . The specs don't even finish to run which is consistent with your experience with kemal.

@robacarp can you check if crystal 1.10.x works for you until I can narrow it down better?

Call graph:
    2490 Thread_45538412   DispatchQueue_1: com.apple.main-thread  (serial)
    + 2490 start  (in dyld) + 2360  [0x1884050e0]
    +   2490 main  (in crystal-run-spec.tmp) + 12  [0x1045870a0]  main.cr:141
    +     2490 *Crystal::main<Int32, Pointer(Pointer(UInt8))>:Int32  (in crystal-run-spec.tmp) + 108  [0x10476dd90]  main.cr:59
    +       2490 *Crystal::exit<Int32, (Exception+ | Nil)>:Int32  (in crystal-run-spec.tmp) + 40  [0x10476de28]  main.cr:64
    +         2490 *Crystal::AtExitHandlers::run<Int32, (Exception+ | Nil)>:Int32  (in crystal-run-spec.tmp) + 148  [0x1046cc844]  at_exit_handlers.cr:14
    +           2490 ~procProc(Int32, (Exception | Nil), Nil)@/Users/bcardiff/Downloads/crystal-1.11.2-1/src/spec/dsl.cr:208  (in crystal-run-spec.tmp) + 76  [0x1045ed084]  dsl.cr:216
    +             2490 *Spec::execute_examples:Nil  (in crystal-run-spec.tmp) + 28  [0x1046f9d3c]  dsl.cr:234
    +               2490 *Spec::RootContext#run:Nil  (in crystal-run-spec.tmp) + 28  [0x104714e9c]  context.cr:158
    +                 2490 *Spec::RootContext@Spec::Context#internal_run:Nil  (in crystal-run-spec.tmp) + 200  [0x104714f70]  context.cr:18
    +                   2490 *Spec::ExampleGroup#run:Nil  (in crystal-run-spec.tmp) + 364  [0x10474500c]  context.cr:340
    +                     2490 *Spec::ExampleGroup@Spec::Context#internal_run:Nil  (in crystal-run-spec.tmp) + 144  [0x10474516c]  context.cr:18
    +                       2490 *Spec::Example#run:Nil  (in crystal-run-spec.tmp) + 1208  [0x104742340]  example.cr:38
    +                         2490 *Spec::Example#internal_run<Time::Span, Proc(Nil)>:(Array(Spec::Result) | Nil)  (in crystal-run-spec.tmp) + 212  [0x104742444]  example.cr:50
    +                           2490 ~procProc(Nil)@spec/session_spec.cr:9  (in crystal-run-spec.tmp) + 708  [0x1045ee4f8]  session_spec.cr:20
    +                             2490 ~procProc(FSWatch::Session, Channel(FSWatch::Event), String, Nil)@spec/session_spec.cr:41  (in crystal-run-spec.tmp) + 192  [0x1045ed9fc]  session_spec.cr:47
    +                               2490 *File::join<String, String>:String  (in crystal-run-spec.tmp) + 64  [0x1046a5d38]  file.cr:852
    +                                 2490 *Path::new<String, String>:Path  (in crystal-run-spec.tmp) + 64  [0x104661f8c]  path.cr:109
    +                                   2490 *Path#join<String>:Path  (in crystal-run-spec.tmp) + 872  [0x104662310]  string.cr:254
    +                                     2490 *GC::malloc_atomic<UInt32>:Pointer(Void)  (in crystal-run-spec.tmp) + 16  [0x10462fc70]  gc.cr:88
    +                                       2490 *GC::malloc_atomic<UInt64>:Pointer(Void)  (in crystal-run-spec.tmp) + 12  [0x10462fc84]  boehm.cr:152
    +                                         2490 GC_malloc_kind_global  (in crystal-run-spec.tmp) + 60  [0x1047f3048]
    +                                           2490 _pthread_mutex_firstfit_lock_slow  (in libsystem_pthread.dylib) + 248  [0x188780af8]
    +                                             2490 _pthread_mutex_firstfit_lock_wait  (in libsystem_pthread.dylib) + 84  [0x188783168]
    +                                               2490 __psynch_mutexwait  (in libsystem_kernel.dylib) + 8  [0x188748524]

Specs are failing on Linux (Ubuntu 20.04, fswatch 1.14, Crystal 1.11.2) with or without preview_mt: they detect nothing, so I can't reproduce.

FSWatch.watch will not block the main fiber. I think the program is finishing right away.

Same behavior. That's the expected behavior: watch won't block. I added a sleep and it's working correctly. Weirdly the specs won't pass 🤷

@bcardiff on Crystal 1.10.1, it is indeed working. That'll do fine for me, thank you!

(Half) Found it!

I bisected the problem to crystal-lang/crystal#14130
I am not sure why that is causing issues yet.

But the good thing is that at least for now you can monkey-patch your project with the following code and it should work with Crystal 1.11.x (and Crystal master)

lib LibGC
  $stackbottom = GC_stackbottom : Void*
end

module GC
  def self.current_thread_stack_bottom
    {Pointer(Void).null, LibGC.stackbottom}
  end

  def self.set_stackbottom(stack_bottom : Void*)
    LibGC.stackbottom = stack_bottom
  end
end