Disabling garbage collection while loading an extension (fix for SU2024 startup crashes)

Since Profile Builder 4 has been released and especially after SU2024 was released, we (mind.sight.studios) have received several reports of SketchUp crashing during startup.

These crashes occur while extensions are loading and typically no bugsplat reports are generated. However, by examining the SketchUp log file in the temp folder, we can see that the crash would occur while loading Profile Builder 4.

Today, I found a solution to these crashes by disabling Ruby garbage collection while Profile Builder 4 is being loaded. Once the extension is loaded, I then re-enable GC.

GC.disable
load extension rb files here
GC.enable

So far, this minor change seems to fix the crashes, at least on my system. Great!

I don’t see any significant downside to this workaround. Do any extension developers foresee any problem doing this?

Ideally, I would also dig into my own extension code and try to figure out why the garbage collector is causing this crash. Changing local variables to instance variables, for example, is one way to prevent this situation.

However, in the case of Profile Builder 4, the crashes are related to our use of Ruby Encoder and I believe it is the Ruby Encoder library implementation that is causing the GC issues, not our own code. I am unable to modify the Ruby Encoder library, nor would I risk doing so.

The only bit of exposed ruby code that is used to load an encoded file looks like this:

# RubyEncoder v3.0.1
if !defined?(RGLoader) then
  _d = _d0 = File.expand_path(File.dirname(__FILE__));
  while true do
    _f = _d + '/rgloader/loader.rb'; load _f and break if File.exist?(_f); _d1 = File.dirname(_d);
    if _d1 == _d then
      break if defined?(RGLoader); raise LoadError, "Ruby script '" + __FILE__ + "' is protected by RubyEncoder and requires a RubyEncoder loader to be installed. Please visit the https://www.rubyencoder.com/loaders/ RubyEncoder web site to download the required loader and unpack it into '" + _d0 + "/rgloader/' directory in order to run this protected file."; exit;
    else
      _d = _d1;
    end;
  end;
end; RGLoader_load('ENCODED_RUBY_VERY_LONG_STRING');

This snippet is generated by Ruby Encoder when an rb file is encoded. This code is not inside any module or class. Could that be a reason for the GC issue?

Anyway, I wanted to share this solution in case other developers are experiencing similar issues.

Is this an issue on Windows, macOS, or both?

You might try Ruby 3.2.4 on Windows SU, see forum post.

Some of the changes between 3.2.2 and 3.2.4 might affect the issue, not sure…

Generally, all code should be inside a namespace module to prevent defining anything at the top level which becomes global.

This speaks to your question …

If all of your code is within a company namespace module (including local variables) then there should not be any clashes with other Ruby core, API, extension or gem code. Within your company namespace separating your extensions from one another in submodules prevents them from tainting each other.

I agree. The generated snippet might be the culprit. It can call Kernel#exit which used to always crash SketchUp because it would terminate SketchUp’s Ruby subprocess. IMO, using this method is poor practice.
At some point, the SketchUp Extensibility team “tweaked” it so it would not crash in normal use. But perhaps we now have a regression in Ruby 3.2 during the load cycle?

In addition, that snippet uses a dangerous syntax while true … which has the potential of generating a stack overflow. A proper conditional expression should be used.

Of less concern is the frivolous use of the File#expand_path method. This method is dumb. It is a simple concatenation method that can create invalid paths. It concatenates whatever the argument string is onto the end of the current working directory path, whether that will be valid or not. It is not necessary to wrap File.dirname(__FILE__) inside this method as the interpreter __FILE__ function returns an absolute path, so the dirname call will also.

I don’t see much of a problem with temporarily disabling GC. You could also try wrapping the snippet in a RGLoader_Temp module and see if Garbage Collection was trying to GC the local variables in the snippet whilst it loaded the loader file.

Actually, looking at the snippet again, the exit would never be called as a LoadError is raised in the preceding statement. So the exit call is itself frivolous.

Would something like this be more stable?

# encoding: UTF-8
#
module MindSight
module ProfileBuilder
module Temp

  attr_reader :error

  code_string = 'ENCODED_RUBY_VERY_LONG_STRING'

  # RubyEncoder v3.0.1
  begin
    # Make sure the RGLoader is loaded:
    if !defined?(RGLoader)
      target_dir = this_dir = File.dirname(__FILE__)
      # Look for 'loader.rb' in subdirectory 'rgloader' of this directory:
      loader_path = File.join(this_dir,'rgloader/loader.rb')
      if File.exist?(loader_path)
        load(loader_path)
      else
        # Search up the path chain for the loader ...
        pointer_dir = File.dirname(this_dir)
        # ... until a sucessful load OR we reach the root of this directory's drive:
        begin
          target_dir = pointer_dir
          loader_path = File.join(target_dir,'rgloader/loader.rb')
          load(loader_path) if File.exist?(loader_path)
          pointer_dir = File.dirname(target_dir)
        end until defined?(RGLoader) || pointer_dir == target_dir
      end
      #
      unless defined?(RGLoader)
        text = "Ruby script '#{__FILE__}' is protected by ".concat(
          'RubyEncoder and requires a RubyEncoder loader to be installed. ',
          'Please visit the https://www.rubyencoder.com/loaders/ RubyEncoder',
          ' web site to download the required loader and unpack it into',
          " '#{this_dir}/rgloader/' directory in order to run this protected file."
        )
        raise LoadError, text
      end
    end
  rescue
    puts "Error finding RGLoader ..."
    puts error.inspect
    puts error.backtrace
    @error = error
  else
    begin
      GC.disable
      RGLoader_load(code_string)
      GC.enable
    rescue => error
      puts "Error loading Encoded extension ..."
      puts error.inspect
      puts error.backtrace
      @error = error
      raise # the error to SketchUp's LoadErrors dialog
    end
  ensure
    code_string = nil # so it's not taking up memory
    this_dir = target_dir = pointer_dir = loader_path = nil
  end

end # Temp
end # ProfileBuilder
end # namespace module

Thanks Dan - I appreciate the discussion and ideas. However, all of the files get generated automatically using the Ruby Encoder command line script. There are a lot of source files and it would take me a long time wrap these in a module unless I create a post-processing script to read the encoded files and re-write them inside the suggested module wrappers. Not hard to create this script but not something I am prepared to experiment with at the moment.

All of the files have this defined?(RGLoader) and “loader.rb” file search routine ?