What can I do to speed up my code?

My code is used for getting an array that containing all materials using in scene (I don’t use Sketchup.active_model.materials because it contains unused materials like material of hidden object).

The code runs so slow, it tooks 2 minutes for the test file I provide below. How can I improve my code?

Here is test file: Upload Files | Free File Upload and Transfer Up To 10 GB

def get_all_mtls()
	current_time = Time.now
	formatted_time = current_time.strftime("%d/%m/%Y %H:%M")
	puts "Start time: #{formatted_time}"

	all_entities = Sketchup.active_model.entities 

	def self.get_mtl(entities, all_materials_using = [])
		entities.each do |entity|
			if entity.is_a?(Sketchup::Group) || entity.is_a?(Sketchup::ComponentInstance) 

				entities_inside = entity.definition.entities
				self.get_mtl(entities_inside, all_materials_using)

			elsif entity.class == Sketchup::Face
				all_materials_using << entity.material 
			end
		end
		all_materials_using = all_materials_using.uniq
	end
	
	array_mtls = self.get_mtl(all_entities)
	
	current_time = Time.now
	formatted_time = current_time.strftime("%d/%m/%Y %H:%M")
	puts "End time: #{formatted_time}"
	
	array_mtls
end 
	
my_all_mtls_array = get_all_mtls()

(1) Only do inside the timed block what is actually part of the task at hand.

There is no need to add slow work like formatting the start time into a string containing the day, month and year or sending slow IO to the console. So, at the beginning of the outer method I would use …

def get_all_mtls()
	start_time = Time.now

… and then defer calculating the elapsed time until after the end of the task. So, at the end of the method …

	finish_time = Time.now
	elapsed_time = finish_time.to_f - start_time.to_f
	puts "Elapsed time: #{elapsed_time} seconds (#{elapsed_time/60.0} minutes)"
	
	array_mtls
end

Like, who is going to really care what the date is when the task is run?

(2) Although it is allowed, it is kind of weird to define a method inside another method.

It is more likely to be maintainable and better readable if the nested method is defined outside by itself.

But, the other option is to instead define it as a Proc object if you desire that it go out of scope when the outer method returns so that the proc instance gets garbage collected.


There may be a better way of walking the model or the definition list, but I’ll wait until Steve posts …

1 Like

Thanks for waiting @DanRathbun. I’m working on streamlining the OP’s code, which is very inefficient, given that the model has almost 2,800,800 faces in it!

Here’s a reworked algorithm that uses Hashes to remember what it has already seen instead of a gigantic Array. That way it never processes the same Group or Component more than once and doesn’t need to do a uniq on anything. On my Mac it runs in one second (!). You will want to change the main module name from SLBPlugins to something of your own. The last line in the file shows how to get an array of the material names from the module.

materials.rb (1.2 KB)

One thing to note: this tallies all materials in the entire model, regardless of scene. If you want to count only the objects used in a particular scene, you need to use tags to hide objects that are not visible in that scene and then skip them in the entities.each loop. The Ruby API does not provide any built in methods for testing what is visible in a scene, either in the sense of visible tag or in the sense of not in the view window. You have to do that yourself.

Edit: oops, the first version of the code wasn’t returning the list of found materials. I’ll fix that and reload it in a few minutes.

Edit2: updated

4 Likes

How about some lateral thinking.
Start an operation
Make an array of all of the model’s materials.
Purge the model.materials and make another array of the materials.
Abort the operation, so the purge is undone.
But you still have a pair of arrays, subtract one from the other to work out what’s left.
That is the array of [un]used materials.

4 Likes

It does however have the method to test using instance path whether objects are hidden or using tags that are off:

See: Sketchup::Model#drawing_element_visible?

1 Like

But you have to walk the entities of the model to construct the paths to test, so in the end that isn’t much different from just testing the hidden attribute and tag for each of them. I suppose if drawing_element_visible? is a wrapper atop compiled C code it might be a small amount faster but not an order of magnitude like the algorithm I posted vs the original.

1 Like

Alas, this sounds good but after trying it, I don’t think it can work. I don’t think the purged materials are actually removed until the operation is committed?

OK, how about committing it, so you now have the two arrays and then doing a Sketchup.undo to go back as it was.
Does that work ?
I can’t try it as I’m away from my PC at the moment…
Or perhaps keep the array output, [manually] close the model, without saving and immediately reopening that model…
Or before starting save the model as a temp SKP file, do the purging on the copy and you have the arrays, then discard the copy OR original as desired…

The OP hasn’t clearly explained the purpose of his code…
If he just wants to purge unused materials then there’s ready a method…

There must be a bug in my code! It yields 98 used materials, whereas after doing a purge unused via model info statistics, model info reports 107 remaining? I don’t have time right now to debug it, but be warned!

Perhaps not. Remember that “they” changed the API to hide image and layer owned materials from the Materials collection iterator.

I had a few spare moments and found the bug(s):

  • the code wasn’t capturing materials applied to groups or component instances (this bug was present in the OP’s code too)
  • there are 6 section planes in the model, and their materials are hidden from the Ruby API per @DanRathbun’s post above (image and layer/tag materials would also be missed). I would have to do something akin to what he wrote in the referenced API Tracker item to get a count of these hidden materials. Haven’t done that yet.

Here’s a revised version of my code. It finds 101 used materials, and when adding for the 6 section planes, it matches the 107 reported by model info after a purge.

materials.rb (1.3 KB)

1 Like

Another detail: layers/tags don’t have a material, they just have a color. But that color isn’t included in the model info count nor in the materials editor window, so they can be ignored. Not sure yet about images.

Handling Images turned out to be a bit convoluted due to the funky way they are managed in the Ruby API. An Image is an Entity found in some Entities collection. Like a Group, it has an associated ComponentDefinition, but unlike a Group, there is no method to get that CD from the Image itself. Instead, you have to search the model’s Component Definition collection looking for ones that are marked as images and then examine the instances of each such CD to determine whether one of them matches the Image object in the model. To compound the confusion, these special Component Definitions are not counted as such in the Model Info Statistics, but the total number of Image objects (i.e. instances of those CDs) is reported separately instead. To further the confusion, the statistics count a Material for each image CD, but these Materials are not included in the Materials window. And, of course, if the image is exploded it becomes the Texture associated with a Material, and then it does appear in the Materials window but no longer as an image instance.

Here’s yet another revision that adds the section plane and image materials to the count so that it matches what’s seen in the model info statistics. To illustrate what’s available, this prints the total number of materials, including section planes and images, the number of section planes, and the number of distinct images.

materials.rb (2.2 KB)

4 Likes

I ran the code and it also took a second, unbelievable! Thank you very much!

1 Like

Thanks you for the helpful advice!

1 Like