I wonder is someone invested work in debugging the solver speed recently.
I tried to build an assembly for my project, for now it's a slider with some parts attached to it.
As far as I understand the attachment is the "cheapest" computational constraint, so all my models are attached, besides the slider that use 2xPointOnLine and SameOrientation constraints to have proper alignment with the channels it's slides on.
About 20 models in total in the assembly. All are imported from STEP files. Some are sub-assemblies with very few parts.
I was discouraged by the speed the slider is able to move on the screen when I try to drag it on the free-for-movement axis.
When only the slider moves (disabling the attachments for other parts that should move), it's not smooth, jumps on the screen, but pretty usable.
Having about 10 parts attached to that slider reduces the speed significantly, something that is more in the non-usable area. So I started to add timestamps in the code to see where the time is spent...
I'm not familiar with the code yet, so have some questions to understand it better.
The function I started with is the AsmMovingPart::move() from mover.py, which takes about 254mSec to complete.
It's very hard for me to believe that the screen updates 4 times per second, probably there are other things done before and after that function call, but I continue with the investigation since the solver is called from that function.
Next stop are the solve() and _solve() functions from solver.py:
The assembly.recompute(True) call takes about 20mSec (7.8%)
==> Do we have to recompute the assembly all the time before the solver?
==> We're dragging the same assembly here, nothing changed between the calls to that function in the loop of the move events.
The creation of the Solver() instance (the Solver(assembly,reportFailed,dragPart,recompute,rollback) code) takes about 231mSec (91%), so I dived into the Solver::__init__() code to understand what it does there.
The Constraint.getFixedParts() takes about 18mSec (7%)
==> Again, we're dragging the same assembly, can't we use the same list and not re-build it for every call?
The following loop (constraints preparation, didn't dive deeper to see what it's doing), takes 23mSec (9%)
Code: Select all
for cstr in cstrs:
self.system.log('preparing {}',cstrName(cstr))
self.system.GroupHandle += 1
ret = Constraint.prepare(cstr,self)
if ret:
if isinstance(ret,(list,tuple)):
for h in ret:
if not isinstance(h,(list,tuple)):
self._cstrMap[h] = cstr
else:
self._cstrMap[ret] = cstr
The solver-neto execution by calling self.system.solve() takes about 11.5mSec (4.5%)
The following code (looks like parts placement updates) takes 24.6mSec (9.7%).
Not familiar with the code to understand if something can be optimized here...
Code: Select all
touched = False
updates = []
for part,partInfo in self._partMap.items():
if partInfo.Update:
updates.append(partInfo)
if part in self._fixedParts:
continue
if utils.isDraftWire(part):
changed = False
points = part.Points
for key,h in partInfo.EntityMap.items():
if not isinstance(key, str) or\
not key.endswith('.p') or\
not key.startswith('Vertex'):
continue
v = [ self.system.getParam(p).val for p in h.params ]
v = FreeCAD.Vector(*v)
v = partInfo.Placement.inverse().multVec(v)
idx = utils.draftWireVertex2PointIndex(part,key[:-2])
if utils.isSamePos(points[idx],v):
self.system.log('not moving {} point {}',
partInfo.PartName,idx)
else:
changed = True
self.system.log('moving {} point{} from {}->{}',
partInfo.PartName,idx,points[idx],v)
if rollback is not None:
rollback.append((partInfo.PartName,
part,
(idx, points[idx])))
points[idx] = v
if changed:
touched = True
part.Points = points
else:
params = [self.system.getParam(h).val for h in partInfo.Params]
p = params[:3]
q = (params[4],params[5],params[6],params[3])
pla = FreeCAD.Placement(FreeCAD.Vector(*p),FreeCAD.Rotation(*q))
if isSamePlacement(partInfo.Placement,pla):
self.system.log('not moving {}',partInfo.PartName)
else:
touched = True
self.system.log('moving {} {} {} {}',
partInfo.PartName,partInfo.Params,params,pla)
if rollback is not None:
rollback.append((partInfo.PartName,
part,
partInfo.Placement.copy()))
partInfo.Placement.Base = pla.Base
partInfo.Placement.Rotation = pla.Rotation
setPlacement(part,pla)
if utils.isDraftCircle(part):
changed = False
h = partInfo.EntityMap.get('Edge1.c',None)
if not h:
continue
v0 = (part.Radius.Value,
part.FirstAngle.Value,
part.LastAngle.Value)
if part.FirstAngle == part.LastAngle:
v = (self.system.getParam(h.radius).val,v0[1],v0[2])
else:
params = [self.system.getParam(p).val for p in h.params]
p0 = FreeCAD.Vector(1,0,0)
p1 = FreeCAD.Vector(params[0],params[1],0)
p2 = FreeCAD.Vector(params[2],params[3],0)
v = (p1.Length,
math.degrees(p0.getAngle(p1)),
math.degrees(p0.getAngle(p2)))
if utils.isSameValue(v0,v):
self.system.log('not change draft circle {}',
partInfo.PartName)
else:
touched = True
self.system.log('change draft circle {} {}->{}',
partInfo.PartName,v0,v)
if rollback is not None:
rollback.append((partInfo.PartName, part, v0))
part.Radius = v[0]
part.FirstAngle = v[1]
part.LastAngle = v[2]
==> Looks like that's the main place that should be optimized somehow... Will try to deep dive into that function to understand.
In summary, the slowness is absolutely not the solver fault as I thought, but the FreeCAD/Assembly3 code that should be re-checked to see if it can be optimized somehow.
If someone who is familiar with the code can comment, it will be very helpfull to go forward and prepare a change if possible.
Thanks