leonardorame

leonardorame OP

I created this Gist with the C++ side: ssr.cc · GitHub

0
Post #11
jhogberg

jhogberg

Erlang Core Team

Thanks, having read the NIF and DCMTK source it’s very likely that this is a thread-safety issue. None of the DCMTK functions you use are documented as thread-safe and I saw too many thread-unsafe things to count while reading their source.

That the problem went away after commenting-out lines 27 through 38 also suggests that this is the case.

Try caching results instead of resources if width/height are mostly static, or if they’re very dynamic, create a new process for each instance and store those in the cache. When a new request comes in, ask the cached process to scale the image.

1
Post #12
leonardorame

leonardorame OP

Thanks @jhogberg, yes, one process per instance could be the answer. Now how can I start learning about that?, any pointer?.

0
Post #13
jhogberg

jhogberg

Erlang Core Team

You’re welcome :slight_smile:

The gist of it would be:

  • When there’s a cache miss:
    1. Spawn a process that loads the file, then waits for requests to rescale the image (you might want to wait with a timeout to avoid having too many of these processes hanging around).
    2. Store that process’ pid in the cache.
    3. Ask said process to rescale the image.
  • When there’s a cache hit:
    1. Ask the cached process to rescale the image. If that fails (e.g. if we raced with the process dying due to timeout), treat it as a cache miss.

You’ll also want to fix all the memory leaks in your NIF module. At the very least, you need to free the _dicomimage after you’re done. This will be a lot easier if you fuse createScaledImage, setMinMaxWindow and getPNG into a single function.

1
Post #14
leonardorame

leonardorame OP

Hi again @jhogberg, before spawning new processes I’m fixing memory leaks and trying to understand when the segmentation fault happens and found it crashes when the same cached resource is requested more than once at the same time.

In one of your answers you said Try caching results instead of resources, by this do you mean using enif_make_binary instead of enif_make_resource?

0
Post #15
jhogberg

jhogberg

Erlang Core Team

Yes, that’s consistent with the problems I found in the source.

No, I meant putting finished PNG images into your ImageCache.

0
Post #16
leonardorame

leonardorame OP

Yes I thought about that. The problem is when I need to resize the original image or apply window/level (similar to brightness & contrast) I will be forced to reload the image from disk, and that isn’t efficient.

I also was thinking about creating a binary of the image and pass back and forth between the NIF and the Elixir program, but that also adds overhead, because I must create in-memory copies of the whole image.

It must be a way to only share pointers to the data created by the NIF. The issue with that is that I must be very careful about memory leaks and race conditions.

0
Post #17
jhogberg

jhogberg

Erlang Core Team

To quote myself:

To word it differently, spawn a process whose only job is to rescale a certain image, then put the pid of that process into ImageCache. The process would load the image from disk once, and use the same resource to rescale images when requested to do so.

If several requests for the same image come in concurrently, they would all ask the same process to do this, but you wouldn’t need to worry about races since the process only handles one request at a time, and would finish them in the order they came in.

Makes sense?

0
Post #18
leonardorame

leonardorame OP

Yes, of course that makes sense. Now I need to figure out how to do it.

0
Post #19
leonardorame

leonardorame OP

Hi again @jhogberg, I’m implementing your suggested solution using an Agent, but the program still crashes when a call to createScaledImage, for the same SOPInstanceUID is made more than once at the same time.

Please take a look at the updated image function:

  def image(conn, params) do                                                                                                                                                                                                                 
    sopinstanceuid = params["sopinstanceuid"]                                                                                                                                                                                                
    {width, ""} = Integer.parse(params["width"])                                                                                                                                                                                             
    {height, ""} = Integer.parse(params["height"])                                                                                                                                                                                           
    pid = ImageCache.get(sopinstanceuid)                                                                                                                                                                                                     
    if pid do                                                                                                                                                                                                                                
      IO.puts("====> CACHED! <====")                                                                                                                                                                                                         
      IO.puts "Pid: #{inspect pid}"                                                                                                                                                                                                          
      dcmff = Agent.get(pid, fn map -> Map.get(map, :ff) end)                                                                                                                                                                                
      scaledImage = Dcmtknif.createScaledImage(dcmff, width, height, 0, 0);                                                                                                                                                                  
      IO.puts("====> createScaledImage <====")                                                                                                                                                                                               
      wlImage = Dcmtknif.setMinMaxWindow(scaledImage)                                                                                                                                                                                        
      IO.puts("====> setMinMaxWindow <====")                                                                                                                                                                                                 
      pngimage = Dcmtknif.getPNG(wlImage)                                                                                                                                                                                                    
      IO.puts("====> getPNG <====")                                                                                                                                                                                                          
      conn                                                                                                                                                                                                                                   
      |> put_resp_content_type("image/png")                                                                                                                                                                                                  
      |> send_resp(200, pngimage)                                                                                                                                                                                                            
    else                                                                                                                                                                                                                                     
      im = ApiController.image(sopinstanceuid)                                                                                                                                                                                               
      IO.puts(im.imagepath)                                                                                                                                                                                                                  
      if File.exists?(im.imagepath) do                                                                                                                                                                                                       
        IO.puts("====> NOT CACHED, LOADING FROM FILE <====")                                                                                                                                                                                 
        dicomfile = im.imagepath                                                                                                                                                                                                             
        IO.puts("Antes de load()")                                                                                                                                                                                                           
                                                                                                                                                                                                                                             
        {:ok, pid} = Agent.start_link(fn -> %{} end)                                                                                                                                                                                         
        ImageCache.put(sopinstanceuid, pid)                                                                                                                                                                                                  
        Agent.update(pid, fn map -> Map.put(map, :ff, Dcmtknif.load(dicomfile)) end)                                                                                                                                                         
        dcmff = Agent.get(pid, fn map -> Map.get(map, :ff) end)                                                                                                                                                                              
        IO.puts("Antes de createScaledImage")                                                                                                                                                                                                
        scaledImage = Dcmtknif.createScaledImage(dcmff, width, height, 0, 0);                                                                                                                                                                
        IO.puts("Antes de setMinMaxWindow")                                                                                                                                                                                                  
        wlImage = Dcmtknif.setMinMaxWindow(scaledImage)                                                                                                                                                                                      
        pngimage = Dcmtknif.getPNG(wlImage)                                                                                                                                                                                                  
        conn                                                                                                                                                                                                                                 
        |> put_resp_content_type("image/png")                                                                                                                                                                                                
        |> send_resp(200, pngimage)                                                                                                                                                                                                          
      else                                                                                                                                                                                                                                   
        conn                                                                                                                                                                                                                                 
        |> send_resp(404, "File not found")                                                                                                                                                                                                  
      end                                                                                                                                                                                                                                    
    end                                                                                                                                                                                                                                      
  end

When call createScaledImage once everything is ok, but if the same image is requested more than once, the Agent returns the correct PID, but apparently it is not isolated. Here’s the output just before the crash:


====> CACHED! <====
[info] GET /image
====> CACHED! <====
[info] GET /image
[debug] Processing with Prueba3Web.PageController.image/2
  Parameters: %{"height" => "101", "sopinstanceuid" => "1.3.46.670589.11.76003.5.0.848.2022080115090557013", "width" => "101"}
  Pipelines: [:browser]
[debug] Processing with Prueba3Web.PageController.image/2
  Parameters: %{"height" => "101", "sopinstanceuid" => "1.3.46.670589.11.76003.5.0.848.2022080115090557013", "width" => "101"}
  Pipelines: [:browser]
Pid: #PID<0.649.0>
Pid: #PID<0.649.0>
====> createScaledImage <====
zsh: segmentation fault (core dumped)  mix phx.server
0
Post #20