James Stanley


How to abort calculations in OpenCascade

Thu 27 February 2025
Tagged: software

Hands up, how many times has this happened to you? There you are, minding your own business, designing some sick new parts for your 3d-printed hoverbike, you go to make a PolarPattern of 3 or 4 sweet ridges just to really finish it off and BOOM your hand slips and you accidentally ask for 3 million. FreeCAD hangs for the rest of the week and you feel your will to live slipping away. If only you could cancel the task and change 3 million to a smaller number!

Over the past ~week I've worked on an improvement to FreeCAD that would allow aborting long-running operations. I went through several iterations of the implementation, none of them are flawless.

I really want this feature in FreeCAD, so this post is firstly a description of how you can abort calculations, and get progress reports, in OpenCascade, and secondly a description of my existing Pull Requests. That if you want to work on this you might start a bit further ahead than I did.

OpenCascade is an open-source CAD kernel. Mainly it is FreeCAD's CAD kernel. FreeCAD does (all?) document recomputes on the main thread. That means the user interface is totally blocked while it is working. Sometimes this is annoying because you realise you made a mistake and you want to make some changes and you don't even want the result of the recompute, but your only options are wait for it to finish calculating, or kill FreeCAD and lose all unsaved work.

Currently the recompute needs to be on the main thread, for 2 reasons:

I. OpenCascade

The general structure is that you provide OpenCascade with an implementation of its Message_ProgressIndicator class. You implement 2 methods:

And then when you call into OpenCascade it wants you to pass it a Message_ProgressRange. You get that from Message_ProgressIndicator.Start() (implemented by OpenCascade).

So that's all you need to know to be able to interrupt OpenCascade and show a progress indicator.

I don't actually think in Object-Oriented, and at first I thought I was meant to instantiate OpenCascade's Message_ProgressIndicator and call UserBreak() when the user clicks "abort". This obviously did nothing and I initially thought it just didn't work.

OpenCascade documentation:

"ProgressRange" and "ProgressScope" are to do with allowing sub-operations to report progress of their sub-operation, and have it show up correctly as the appropriate percentage of the overall total operation. I never had to look at it, I just pass my progressIndicator.Start() into OpenCascade at the top level and let the scope/range stuff happen internally to OpenCascade. But if you needed to do multiple slow OpenCascade operations you might want to use it.

There is some example code, from the OpenCascade perspective, on Kirill Tartynskih's blog.

II. My Pull Requests

I have implemented three different solutions:

Attempt 1: worker in child process

"When in doubt, fork it out"
-- ChatGPT

Serialise the inputs and a description of the operation. Send the serialised description over a pipe to a worker process. The worker process sets the calculation going with OpenCascade, meanwhile the main process displays an "abort" dialog. If you click "abort" the child process gets killed, otherwise the child process eventually finishes, serialises its results, and sends them to the main process.

ProsCons
  • Can interrupt OpenCascade without consent (i.e. even if it's not checking UserBreak())
  • Every different type of OpenCascade operation needs specific support (I only implemented "BooleanOperation", which covers a lot of it but not everything)
  • Serialising/deserialising the inputs and outputs is potentially slow
  • Windows can't do fork(), pipe(), etc. so would need a separate implementation

This was my first attempt. I did this before I found out that OpenCascade provides the UserBreak() mechanism, and I thought the only possible way to interrupt OpenCascade was to run it in a separate process and kill the process.

I was so delighted when I first got this working that I recorded a video demonstration.

I only implemented support for "Fuse" and "Cut" boolean operations. That does get you most of what real-life CAD usage is focused on, but it's not everything. In particular helix, pipe, loft, fillet, chamfer, etc. aren't covered by this and can all be slow, so they would want specific support as well.

PR 19710

Attempt 2: worker in thread

Run the calculation on a worker thread in the main process, and the abort dialog on the main thread. Cross your fingers and close your eyes to the potential unsafe Qt usage. Skip the worker thread and revert to status quo if main thread holds the GIL.

ProsCons
  • No need for a child process
  • Potential unsafe Qt operations
  • Revert to non-abortable status quo if GIL is already held

I did this after I was pointed to the OpenCascade documentation, and Kirill Tartynskih's blog post, and discovered that not only does OpenCascade provide a way to let you abort, but it also reports progress percentage. Jackpot!

I did find that my implementation here actually does cause segfaults due to the unsafe Qt usage, although not on every operation. In particular it seemed perfectly stable when I was doing "normal" CAD operations, but I found that if I assigned an "alias" to a cell in the Spreadsheet workbench, it would instantly segfault every time on the no-op recompute that is triggered when you apply the alias.

So I don't know the specific reason that that matters, but I do know that it is unsafe and unstable.

I also tried checking whether the main thread currently holds the GIL, and if so releasing it, re-acquiring it in the worker thread, and undoing that after the operation is done, but I found that that caused segfaults. I don't know why. Possibly I was doing it wrong but if so I couldn't work out how to do it right. So I just settled for skipping the abort dialog if the main thread already holds the GIL, at least that way you sometimes get to abort.

PR 19763

Attempt 3: dialog in child process

Start a child process to show the "abort" dialog, run the calculation on the main thread in the main process, and have the child process send a SIGHUP if the user clicks "abort", and have the main process's SIGHUP handler tell OpenCascade to stop working.

ProsCons
  • Computation on main thread without need for serialising/deserialising
  • Still need a separate implementation for Windows
  • Main process throws up "FreeCAD is not responding" warnings

I think this is basically as good as you can do. There are no regressions, because in every case that this triggers "FreeCAD is not responding", the existing FreeCAD codebase also triggers that.

But it brought to everyone's attention the fact that FreeCAD is doing blocking operations on the main thread, which is obviously not what anybody wants.

PR 19796

III. What next?

None of my implementations have been acceptable, and I don't think any of them are going to get merged.

I believe that the FreeCAD maintainers believe that it would be better if FreeCAD did recomputes on a separate thread, in a safe way. We could split off the worker thread at a level that won't have to call back into Python (maybe?), and we could make sure all Gui operations are scheduled on the main thread with QMeta::invokeMethod(). That would then allow the main thread to stay responsive and allow providing a progress bar and abort dialog on the main thread.

I agree that that would be better, of course.

But the FreeCAD codebase is over a million lines of C++. Would you like to audit all of that code to make sure all of the Gui operations that can conceivably be called into via a recompute are using invokeMethod() like good citizens? Would you like to make sure none of it can call into Python code?

I don't think it's going to happen.

In the mean time I think this is a classic case of Perfect being the enemy of Good. I think FreeCAD is strictly a better program if we are able to abort slow operations than if we are not, and I think my third implementation has no regressions versus the status quo.

But I'm firmly at the "hacker" end of the hackers/builders/maintainers spectrum. Maintainers do us a great service by keeping FreeCAD working over the decades. Everyone has their own pet feature that they'd love to hack on to FreeCAD, but it is only thanks to the diligence of the maintainers that FreeCAD is still alive at all. So let's not hate on the maintainers too much.

So I think an ideal solution would look like my second pull request, except with FreeCAD re-architected so that if the recompute needs to call into Gui code or Python code then it does so by some message-passing to the main thread, and then also every code path that can trigger a recompute needs to accept that the recompute is now asynchronous.

If you read this far: maybe you can bring this feature into FreeCAD! If so, please get in touch, I want to help you.

I have done this kind of "synchronous-to-asynchronous" refactor before. In fact at one customer I have done "the same one" 3 times because turning a synchronous application into an asynchronous one is such a monster change that it is basically impossible to be confident that it's correct, and by the time anyone gets the confidence to actually merge it the codebase has diverged so much that you need to start again. For this reason I don't think this will ever happen for FreeCAD, but I'd love to be proven wrong. It is possible that everything that triggers a recompute is already happy for it to be asynchronous, but personally I doubt it.

The trick is to break it down into small incremental changes that can move parts of the application towards being asynchronous without ever leaving it in a broken state. If you try to do the entire thing at once it will never get merged.

Godspeed.



If you like my blog, please consider subscribing to the RSS feed or the mailing list: