Speeding Up Extension Loading

I KNOW that comparing long absolute pathnames against a every member in a large array of long absolute pathnames is going to be a time waster as compared to a simple quick boolean test (the value held in a reference local to the plugin code where it will never clash with any other external reference.)

But for a quick test …

def test(loop = 500)
  loop = loop.to_i

  checkpath = $".last
  t1 = Time.now
  loop.times do |i|
    if $".any? {|path|
      path == checkpath
    }
      true
    end
  end
  t1end = Time.now

  @loaded = false
  t2 = Time.now
  loop.times do |i|
    if !@loaded
      false
    end
  end
  t2end = Time.now
  
  t1time = t1end.to_f - t1.to_f
  t2time = t2end.to_f - t2.to_f

  puts "Results: #{loop} times"
  puts
  puts "Test absolute string pathname comparison: #{t1time} seconds"
  puts
  puts "Test boolean reference comparison: #{t2time} seconds"
  puts
  puts "Conclusion: absolute string pathname comparison is #{t1time/t2time} times longer."

end

500 times …

test
Results: 500 times

Test absolute string pathname comparison: 0.008013010025024414 seconds

Test boolean reference comparison: 0.0 seconds

Conclusion: absolute string pathname comparison is Infinity times longer.

50,000 times …

Results: 50000 times

Test absolute string pathname comparison: 0.7010889053344727 seconds

Test boolean reference comparison: 0.001995086669921875 seconds

Conclusion: absolute string pathname comparison is 351.40774378585087 times longer.

Now …

I may be a bit bombastic in saying “SketchUp startup times suffer” but it’s not the main reason why I do not use the “sketchup.rb” file_loaded? method.

It’s principle that the test is a boolean one, and pushing long pathname strings into a global array and then walking that array to compare loaded files is not the best practice nor the fastest.

It’s better and faster to rely upon the internal extension itself to know it’s own loaded state.

There are no features in “sketchup.rb” that are not better done by the extension author and their code itself, and then the extension does not need to even require “sketchup.rb”.

2 Likes

Yea - agree. sketchup.rb is of limited use.

Though, seeing how file_loaded is so widely used, we could at least tweak the internals and make it use a Hash or Set. (Not sure if anyone might be messing with the internals for development purposes - but hey, that’s what you get for poking into internal details.)

2 Likes

I think my biggest problem with file_loaded? is that it more or less poses as a native Ruby feature, but it turns out it’s a somewhat messy almost a hack added by loading a specific Ruby file shipped with SketchUp. If included in a shipped file, it should really be wrapped in a namespace IMO.

How would this eliminate large String comparison ?

Hashmaps and sets don’t compare the content of the element, but creates a hash of it and use as key. Not sure how heavy the hashing algorithm is.

1 Like

Most of this discussion goes way over my head. Fortunately there are the “brains” who understand it all, for that I am grateful.

Have we come to a consensus on what is the best way to handle these basic require statements?

require ‘sketchup.rb’
require ‘extensions.rb’
require ‘langhandler.rb’

I use them universally in my plugins and anything I can do to squeeze a bit more performance out of SketchUp is always a welcome thought.

For my cabmaker plugin I decided to load my dictionary once at startup so users could see my contribution to the extensions menu in their own language.

time to load English translation file (885 lines - 24 K)
0.004693 seconds.

This uses a hash

I don’t require sketchup.rb as I never use what it defined.

I do require extensions.rb as all my plugins are extensions, and as they would function in the hypothetical case of no other extension being installed (not currently possible as shipped extensions (sadly) can’t be uninstalled).

I never use langhandler due to its limitations. Instead I use my own Ordbok library that I include in all my localized extensions. Among other things it supports interpolation, so you can add numbers and other information inside of strings.

Hash is fast(er).

def test(loop = 500)
  loop = loop.to_i
  checkpath = $".last
  checkpath.freeze

  # The Sketchup.rb implementation uses include?()
  t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  loop.times do |i|
    # if $".any? {|path|
      # path == checkpath
    # }
        # true
    # end
       
    if $".include?(checkpath)
		 true
    end
  end
  t1end = Process.clock_gettime(Process::CLOCK_MONOTONIC)

  # using @loaded boolean
  @loaded = false
  t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  loop.times do |i|
    if !@loaded
      false
    end
  end
  t2end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  
  # using a Hash key lookup
  h = Hash[ $".collect{|s| [s]} ]
  t3 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  loop.times do |i|
  if h.has_key?(checkpath)
		 true
    end
  end
  t3end = Process.clock_gettime(Process::CLOCK_MONOTONIC)

  t1time = (t1end.to_f - t1.to_f) * 1000000.0
  t2time = (t2end.to_f - t2.to_f) * 1000000.0
  t3time = (t3end.to_f - t3.to_f) * 1000000.0

  puts "Looping: #{loop} times"
  puts "Test absolute string pathname comparison: #{"%10.9f" % t1time} useconds"
  puts "Test boolean reference comparison: #{"%10.9f" % t2time} useconds"
  puts "Test Hash reference comparison: #{"%10.9f" % t3time} useconds"
  puts "Conclusion: Boolean comparison is #{(t1time/t2time).round} times faster."
  puts "Conclusion: Hash comparison is #{(t1time/t3time).round} times faster."
  puts
  
  
end


test(0)
test(1)
test(50)
test(100)
test(1)

Since SU2014 (when it distro’d with Ruby 2.x) all the files in the "Tools" folder have been loaded by SketchUp after it loads Ruby String encoding and Rubygems support, … and before it begins processing any of the "Plugins" folders.

You yourself can see this in the load order of the $LOADED_FEATURES array.


My suggestion is if your code does not have a dependency upon "sketchup.rb" then do not put a require statement for it in your code files.

If your code does use any of the methods defined by "sketchup.rb" then you can put it in, but as I suggest in the original post, that you can add a conditional modifier to skip actually calling require if it’s already been loaded by checking if one if it’s features is defined.


The two others you’d also handle the same way.

require 'extensions.rb' unless defined?(SketchupExtension)

… could be at the top of all of your extension registrar files (those in the "Plugins" directories.)


require 'langhandler.rb' unless defined?(LanguageHandler)

… only if your extension uses (memory wasting .strings files) and only at the top of the first file that instantiates a LanguageHandler instance object.

I long since switched to using Ruby hash files as they are loaded into my extension namespaces using Ruby’s own compiled interpreter.

If I was to use a non-rb file, I think I’d go with JSON as they are easily converted into Ruby hashes and visa versa. (ADD: I actually do this for extension options files.)

1 Like

You can see comments in our Hello Cube tutorial:

It doesn’t mention “langhandler.rb”, but I deliberately omitted that since I think it’s too flawed to really recommend. (IMO it should never have been made part of the API - it was only an internal utility.)

1 Like

However, this comment is not true anymore. Since SU2014, when SketchUp began shipping Ruby 2.x, SketchUp has automatically loaded the (3) files in the "Tools" folder before processing any of the “Plugins” folders.

So reality is the opposite of what your “tutorial” teaches.


It only makes sense to have these two methods and the array (or set or hash) of pathnames around during the startup cycle. After that they just waste memory.

It’s just as easy to create a local boolean reference to track the plugin load.

If you want to “hold the coder’s hand” perhaps update the SketchupExtension class to have an internal loaded boolean and automatically set it true when the instance’s load() method returns. Then for loading menus and toolbars, etc., you’d have the pattern …

  unless EXTENSION.loaded?
    # create UI::Commands
    # setup menus
    # setup toolbars
  end

end # module

… where the SketchupExtension instance is referenced within the extension’s module namespace as the constant EXTENSION. In this scenario coder’s wouldn’t need to explicitly initialize the local variable nor explicitly set it true at the end of loading. The former would be done by the constructor, and the latter by the load method.

1 Like

Nope - that’s not the case. We don’t auto-load any of those files. Maybe you are thinking of $LOAD_PATH order? That might have changed with the Ruby 2.0 release.

You are incorrect !
I have a good memory about this loading scenario.
I was part of the discussion about the change at the time.

And I just verified. On a spankin’ new install with only the 4 Trimble extensions installed.
I copied the “Plugins” folder as a backup. Then went through the 4 registrar files and replaced all the registration code with one line …

# DONT LOAD

Meaning the registrar files no longer have ANY code, do not require any other files, and no longer instantiate nor register their own extensions.

After loading the contents of $" (aka $LOADED_FEATURES) is …

"enumerator.so", "thread.rb", "rational.so", "complex.so",
"C:/Program Files/SketchUp/SketchUp 20nn/Tools/RubyStdLib/platform_specific/enc/encdb.so",
"C:/Program Files/SketchUp/SketchUp 20nn/Tools/RubyStdLib/platform_specific/enc/trans/transdb.so",
"C:/Program Files/SketchUp/SketchUp 20nn/Tools/RubyStdLib/platform_specific/enc/windows_1252.so",
"C:/Program Files/SketchUp/SketchUp 20nn/Tools/RubyStdLib/platform_specific/rbconfig.rb",
... many rubygems files ...
"C:/Program Files/SketchUp/SketchUp 20nn/Tools/RubyStdLib/platform_specific/enc/trans/single_byte.so",
"C:/Program Files/SketchUp/SketchUp 20nn/Tools/extensions.rb",
"C:/Program Files/SketchUp/SketchUp 20nn/Tools/langhandler.rb",
"C:/Program Files/SketchUp/SketchUp 20nn/Tools/sketchup.rb",
"C:/Users/Dan/AppData/Roaming/SketchUp/SketchUp 20nn/SketchUp/Plugins/su_advancedcameratools.rb",
"C:/Users/Dan/AppData/Roaming/SketchUp/SketchUp 20nn/SketchUp/Plugins/su_dynamiccomponents.rb",
"C:/Users/Dan/AppData/Roaming/SketchUp/SketchUp 20nn/SketchUp/Plugins/su_sandbox.rb",
"C:/Users/Dan/AppData/Roaming/SketchUp/SketchUp 20nn/SketchUp/Plugins/su_trimble_connect.rb"

… where the SketchUp version year is “20nn”.

Notice that the 3 "Tools" folder files are auto loaded just after all the string encoding and rubygems files, but before the processing of the %AppData% "Plugins" folder. (There are no other extensions, none at all in the %ProgramData% path.)

Notice also that they are loaded in alpha order.

If they were not autoloaded, and if "su_advancedcameratools.rb" still had code, and was the cause of these 3 "Tools" files being loaded, then these 3 files would be listed in a different order. That order would be …

"C:/Program Files/SketchUp/SketchUp 20nn/Tools/langhandler.rb",
"C:/Program Files/SketchUp/SketchUp 20nn/Tools/sketchup.rb",
"C:/Program Files/SketchUp/SketchUp 20nn/Tools/extensions.rb",

… because "sketchup.rb" is required first, but in turn requires "langhandler.rb".
Then "extensions.rb" is required and loaded, and lastly "langhandler.rb" is required but already loaded.

Looking at "extensions.rb" it appears the internal @loaded variable and loaded? method is already implemented.

The first time the files get loaded, they’ll be loaded by the extension’s load() method which does so whilst the @loaded (and the return from loaded?) is false, so the UI elements will get created and loaded using the scenario above.

Once the load is successful, @loaded gets set true and any subsequent manual reload should not recreate UI objects (or run any code inside such conditional blocks.)


So using file_loaded() and file_loaded?() from "sketchup.rb" has probably been unneeded since SU2013.

3 Likes

I stand corrected Dan. I looked closer and we do indeed load files from the Tools directory. (Not sure when that happened though. Difficult tracking changes across our Perforce to Git migration.)

I think it happened with 2014 release using Ruby 2.0 (but I no longer have it installed.) It is as desribed for as early as the 2016 release.

I was thinking that might be the case as well. That was probably right before I joined.