Lazy-loading extension logic


#1

Continuing the discussion from Sketchup 2019 Feature Requests:

it trivial to split the menu items from the logic…

in your extension.rb you point to a menu.rb

    # use 'lazy-loading' by only adding the ruby file containg menu and toolbar items
    PLUGIN = SketchupExtension.new(extension_name, "your_extension_menu")

in your_extension_menu file you load needed images, translations and a ‘logic loader’ command

    # add a cmd for menu, toolbar or shortcut item...
    cmd = UI::Command.new(title) do
      # load the logic containing any require code, resources, etc...
      Sketchup.require File.join(path, "your_extension_logic")
      # and if it's defined under the same namespace...
      your_main_method
    end

you add the command to where required and on activation it first loads all the resources it needs and then runs the items action…

additional commands can load it’s additional requirements and if none are activated during a session the the cost to SU performance is a few menu items…

there are many extensions that load observers, add attributes, materials and even components on SU startup and on every modification…

you only need to track the DC tool or vary to see all the activity that can be generated…

john


#2

Except that (in your example) calling require() is slow, because it needs to check a full absolute pathstring against the HUGE number of strings kept in the $LOADED_FEATURES array.

I’d recommend defining “lazy loaded” module variables to keep track of what needs loading:

# at top of module
@@cmd_support_loaded ||= {}
@@cmd_support_loaded[:title]= false
# ... etc. for other commands this plugin defines

Then down in the command definition …

    # add a cmd for menu, toolbar or shortcut item...
    cmd = UI::Command.new(title) do
      unless @@cmd_support_loaded[:title]
        # load the logic containing any require code, resources, etc...
        Sketchup.require File.join(__dir__, "title_command_method_logic.rb")
        @@cmd_support_loaded[:title]= true
      end
      # and if it's defined under the same namespace...
      title_command_method()
    end

:nerd_face:

If you’re a real true Ruby geek, you could even get fancy and override method_missing() in your module, and have it do the loading when it gets called for a missing command method.

… oh no, I can’t help myself …

module Author
  module SomePlugin

    extend self

    # Constants, module variable declarations,
    # other method definitions, etc., etc., ...

    def method_missing(symbol, *args, &block)
      lazy_loaded_file = File::join(__dir__,"#{symbol}_logic.rb")
      if exist?(lazy_loaded_file)
        Sketchup::require(lazy_loaded_file) # load it
        if block_given?
          method(symbol).call(*args, &block)
        else
          method(symbol).call(*args)
        end
      else
        super # otherwise pass it on up the chain
      end
    end

    # Run once conditional block that loads commands, menus and toolbars

  end
end

#3

If you wait to load the translations until the user interacts with the plugin, the menu itself can’t be translated. Unless you split up the translation into two chunks, one that loads directly and one that leads laters, but that would just make things unnecessary complicated.

Some plugins need observers before the user even interacts with the menu because the observer is the primary way to interact with the extension.

Loading components, materials or setting attributes without user interaction is however not acceptable. I don’t think such a filthy extension would even pass the extension warehouse review. Such behavior would break the modified state of the model, causing SketchUp to ask whether the user wants to save the changes, even if the user hasn’t changed anything.


#4

I’ve always had the translations for the SketchupExtension object separate from the plugin’s translations proper. (There is need to load the core translations until the user decides to switch the extension on.)

But this does not mean I had 2 sets of files. For extension registration, only a few strings need translating so I usually just use a literal Ruby hash or two, pick the strings needed, then clear the hash and set it to nil.


#5

I tend do this for the whole module in the extension.rb as they are needed for the description, etc…

when a particular lang file gets too large I will split it up…

can you cite some examples, I can only think of one where a Sketchup::LayersObserver may be justified…

john


#6

Two set of files or not, the translation is still split up into two places. I would not do that.

I think someone made a plugin that stops faces created in the X Y plane from being drawn back side up. My railroad plugin updates the tracks after they are moved with native move tool. My auto weld plugin also optionally welds edges created by native Follow Me, whether the plugin’s menu has been interacted with in that session or not.


#7

The Extension Manager only needs two (2) strings from the SketchupExtension object (name and description,) in order to display the extension information in it’s list.

Why load an entire LanguageHandler object for the entire extension, when you do not know yet if the user is going to have the extension switched ON ?

To do so, wastes the user’s memory.


Sometimes I just use a case statement to set these rather than even have them in a separate file …

case Sketchup.get_locale
when 'en-US'
  # load English
  ext.name = "My Plugin"
  ext.description = "A plugin that does nifty things ..."
when 'fr'
  # set name and desciption
when 'it'
  # set name and desciption
when # ... etc. 
  # ... etc.
else
  # load English
  ext.name = "My Plugin"
  ext.description = "A plugin that does nifty things ..."
end

Once read by SketchUp these cannot be changed for the session, so there is no reason to have them in memory anymore.


#8

A good starting point for lazy loading might be to implement the singleton design pattern instead of using modules.

require 'singleton'

module Author
  module MyAwesomePlugin

    class Logger
      include Singleton

      def initialize
        @log = File.open("log.txt", "a")
      end

      def log(msg)
        @log.puts(msg)
      end
    end

  end
end

Author::MyAwesomePlugin::Logger.instance.log('message 2')

This makes sure that there will only be one instance of that class, which is only constructed the first time you call the instance. Resources are only assigned in the constructor (in the initialize method) and not during load time, which is the case when working with modules.


#9

We are required to use Author/Company namespacing modules in SketchUp’s shared objectspace.
And then we do like to separate each of our extensions from one another within that namespace.

There is no way to avoid Author::SomePlugin namespace nested modules.
They are required to publish on the Trimble Extension Warehouse.
Improperly namespaced SketchUp extensions have been and will be shunned by the community.


#10

Yes. But that doesn’t mean your logic has to be in a module.
But, for the sake of correctness, I edited my post.


#11

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.