A bit of back information. I have a 3D printer. Yes, I have one. I got myself an Ultimaker: http://blog.ultimaker.com/
Which is a cool machine. And I’ll do a write up one day. But for now, I’m tackling 1 piece of software.
Skeinforge.
Skeinforge is the tool which turns a 3D model into GCode, which are instructions for your 3D printer or CNC mill. And it has 2 major problems, one it’s slow, and secondly it’s DOG SLOW. Turning the gears1.stl from http://www.thingiverse.com/thing:10483 into GCode takes 6:52 minutes on a Core 2 2.33GHz machine. And it’s not the most complex model out there. People have reported convert times of 11 hours!
So, it needs to be faster. And it can be. First, install Psyco: http://psyco.sourceforge.net/ just do it. Skeinforge uses it, and it speeds it up. A LOT, time went down to 1:47. Pysco compiles python code with some JIT compiler. I don’t know the details, but it works. And it’s faster, so that’s good.
Now, bring in the profiler:
python -m cProfile –sort time skeinforge_application/skeinforge.py gear1.stl
Runs python with the profile, sorting on total time spend in a function.
I also noticed, Psyco messes with the profiler. With Pysco the profiler reports all time has been spend in the writeExport function. To which only a single call was made. I think pysco takes over from there and the profiler loses track of it. So profiling runs are done without pysco installed.
If we do this we spot 2 offenders. There are 50898067 of calls made “getStepKey”. And what does this function do… it returns it 2 parameters. As every call takes time, this is just a waste. The only place this function is used is in the same file. Removing all the calls to “getStepKey” and just replacing them with the result saves almost a minute! Bringing the export without Pysco down to 5:58 minutes. That’s a 15% improvement with just a minor code tweak!
Next up, “addPixelToPixelTableWithSteepness”, this function is called 34524828 times, and it spends 34 seconds in it. And calls “addPixelToPixelTable” with (x,y) or (y, x) depending on it’s parameter “isSteep”. Now, this all looks an awful lot like parts of the Bresenham’s_line_algorithm which is good, as that’s a fast line algorithm, but it’s bad to split it into functions! The addPixelToPixelTable is only called from addPixelToPixelTableWithSteepness. So that’s 2 calls overhead for every “pixel”. Also, the “isSteep” is checked for every pixel, while it’s static during the whole line draw. So the test for isSteep can be moved before the looping and just make 2 special cases.
And with those adjustments we are down to 4:54 minutes. That’s a 30% improvement. Now let’s see what psyco does with it. With the code changes and psyco installed we run in 1:36 minute. Which is a 11% improvement. Not as good as without psyco. But maybe with more complex models the time saved will improve.
I did the following patch on the code, which should apply on any version of Skeinforge: A pre-patched file can be find here (from SF35, other SF engines might not work with this file!)
--- skeinforge-35/fabmetheus_utilities/euclidean.py 2011-09-27 10:39:35.000000000 +0200
+++ skeinforge-35-org/fabmetheus_utilities/euclidean.py 2011-09-26 17:34:36.000000000 +0200
@@ -63,7 +63,7 @@
def addElementToPixelList( element, pixelDictionary, x, y ):
'Add an element to the pixel list.'
- stepKey = (x, y)
+ stepKey = getStepKey(x, y)
addElementToListTable( element, stepKey, pixelDictionary )
def addElementToPixelListFromPoint( element, pixelDictionary, point ):
@@ -115,7 +115,7 @@
def addPixelToPixelTable( pixelDictionary, value, x, y ):
'Add pixel to the pixel table.'
- pixelDictionary[(x, y)] = value
+ pixelDictionary[getStepKey(x, y)] = value
def addPixelToPixelTableWithSteepness( isSteep, pixelDictionary, value, x, y ):
'Add pixels to the pixel table with steepness.'
@@ -173,20 +173,12 @@
xBegin = int(round(beginComplex.real))
xEnd = int(round(endComplex.real))
yIntersection = beginComplex.imag - beginComplex.real * gradient
- if isSteep:
- pixelDictionary[( int( round( beginComplex.imag ) ), xBegin)] = None
- pixelDictionary[( int( round( endComplex.imag ) ), xEnd )] = None
- for x in xrange( xBegin + 1, xEnd ):
- y = int( math.floor( yIntersection + x * gradient ) )
- pixelDictionary[(y, x)] = None
- pixelDictionary[(y + 1, x)] = None
- else:
- pixelDictionary[(xBegin, int( round( beginComplex.imag ) ) )] = None
- pixelDictionary[(xEnd, int( round( endComplex.imag ) ) )] = None
- for x in xrange( xBegin + 1, xEnd ):
- y = int( math.floor( yIntersection + x * gradient ) )
- pixelDictionary[(x, y)] = None
- pixelDictionary[(x, y + 1)] = None
+ addPixelToPixelTableWithSteepness( isSteep, pixelDictionary, None, xBegin, int( round( beginComplex.imag ) ) )
+ addPixelToPixelTableWithSteepness( isSteep, pixelDictionary, None, xEnd, int( round( endComplex.imag ) ) )
+ for x in xrange( xBegin + 1, xEnd ):
+ y = int( math.floor( yIntersection + x * gradient ) )
+ addPixelToPixelTableWithSteepness( isSteep, pixelDictionary, None, x, y )
+ addPixelToPixelTableWithSteepness( isSteep, pixelDictionary, None, x, y + 1 )
def addSquareTwoToPixelDictionary(pixelDictionary, point, value, width):
'Add square with two pixels around the center to pixel dictionary.'
@@ -195,7 +187,7 @@
y = int(round(point.imag))
for xStep in xrange(x - 2, x + 3):
for yStep in xrange(y - 2, y + 3):
- pixelDictionary[(xStep, yStep)] = value
+ pixelDictionary[getStepKey(xStep, yStep)] = value
def addSurroundingLoopBeginning( distanceFeedRate, loop, z ):
'Add surrounding loop beginning to gcode output.'
@@ -253,20 +245,12 @@
xBegin = int(round(beginComplex.real))
xEnd = int(round(endComplex.real))
yIntersection = beginComplex.imag - beginComplex.real * gradient
- if isSteep:
- pixelDictionary[(int( round( beginComplex.imag ) ), xBegin)] = value
- pixelDictionary[(int( round( endComplex.imag ) ), xEnd)] = value
- for x in xrange( xBegin + 1, xEnd ):
- y = int( math.floor( yIntersection + x * gradient ) )
- pixelDictionary[(y, x)] = value
- pixelDictionary[(y + 1, x)] = value
- else:
- pixelDictionary[(xBegin, int( round( beginComplex.imag ) ))] = value
- pixelDictionary[(xEnd, int( round( endComplex.imag ) ))] = value
- for x in xrange( xBegin + 1, xEnd ):
- y = int( math.floor( yIntersection + x * gradient ) )
- pixelDictionary[(x, y)] = value
- pixelDictionary[(x, y + 1)] = value
+ addPixelToPixelTableWithSteepness( isSteep, pixelDictionary, value, xBegin, int( round( beginComplex.imag ) ) )
+ addPixelToPixelTableWithSteepness( isSteep, pixelDictionary, value, xEnd, int( round( endComplex.imag ) ) )
+ for x in xrange( xBegin + 1, xEnd ):
+ y = int( math.floor( yIntersection + x * gradient ) )
+ addPixelToPixelTableWithSteepness( isSteep, pixelDictionary, value, x, y )
+ addPixelToPixelTableWithSteepness( isSteep, pixelDictionary, value, x, y + 1 )
def addXIntersectionIndexesFromLoop( frontOverWidth, loop, solidIndex, xIntersectionIndexLists, width, yList ):
'Add the x intersection indexes for a loop.'
@@ -459,7 +443,7 @@
y = int( point.imag * oneOverOverlapDistance )
if not getSquareIsOccupied( pixelDictionary, x, y ):
away.append(point)
- stepKey = (x, y)
+ stepKey = getStepKey(x, y)
pixelDictionary[ stepKey ] = None
return away
@@ -1499,7 +1483,7 @@
squareValues = []
for xStep in xrange(x - 1, x + 2):
for yStep in xrange(y - 1, y + 2):
- stepKey = (xStep, yStep)
+ stepKey = getStepKey(xStep, yStep)
if stepKey in pixelDictionary:
return True
return False
@@ -1515,7 +1499,7 @@
squareValues = []
for xStep in xrange(x - 1, x + 2):
for yStep in xrange(y - 1, y + 2):
- stepKey = (xStep, yStep)
+ stepKey = getStepKey(xStep, yStep)
if stepKey in pixelDictionary:
squareValues += pixelDictionary[ stepKey ]
return squareValues
EDIT
And there is even more, this is just to stupid… for every ‘plugin’ run it checks if other plugins already ran on the GCode. All fine and great, but it does so by splitting the code in lines, for each line replacing text, and then split the string and check it if it contains a certain tag. I replaced the whole function by 6 lines of code. Shaving 40 seconds of the 4:54.
--- skeinforge-35-org/fabmetheus_utilities/gcodec.py 2011-09-20 11:17:43.000000000 +0200
+++ skeinforge-35/fabmetheus_utilities/gcodec.py 2011-09-27 15:09:43.000000000 +0200
@@ -166,24 +166,10 @@
'Determine if the procedure has been done on the gcode text.'
if gcodeText == '':
return False
- lines = archive.getTextLines(gcodeText)
- for line in lines:
- withoutBracketsEqualTabQuotes = getWithoutBracketsEqualTab(line).replace('"', '').replace("'", '')
- splitLine = getWithoutBracketsEqualTab( withoutBracketsEqualTabQuotes ).split()
- firstWord = getFirstWord(splitLine)
- if firstWord == 'procedureDone':
- if splitLine[1].find(procedure) != -1:
- return True
- elif firstWord == 'extrusionStart':
- return False
- procedureIndex = line.find(procedure)
- if procedureIndex != -1:
- if 'procedureDone' in splitLine:
- nextIndex = splitLine.index('procedureDone') + 1
- if nextIndex < len(splitLine):
- nextWordSplit = splitLine[nextIndex].split(',')
- if procedure in nextWordSplit:
- return True
+ if 'procedureDone="'+procedure+'"' in procedure:
+ return True
+ if '(<procedureDone> '+procedure+' </procedureDone>)' in procedure:
+ return True
return False