Skip to content

Reuse context - #259

Open
urschrei wants to merge 1 commit into
mainfrom
reuse_context
Open

Reuse context#259
urschrei wants to merge 1 commit into
mainfrom
reuse_context

Conversation

@urschrei

@urschrei urschrei commented Jun 10, 2026

Copy link
Copy Markdown
Member
  • I agree to follow the project's code of conduct.
  • I added an entry to the project's change log file if knowledge of this change could be valuable to users.
    • Usually called CHANGES.md or CHANGELOG.md
    • Prefix changelog entries for breaking changes with "BREAKING: "

Based on #258, so merge that first. This is part 2 of a 2-part stack which improves perf of Proj::new creation to around 1us per instance.

Comment thread src/proj.rs Outdated
let ctx = unsafe { proj_context_clone(self.ctx) };
crs_to_crs_from_pj(ctx, self, target_crs, area, options)
let ctx = unsafe { proj_context_clone(self.ctx()) };
crs_to_crs_from_pj(Context::Owned(ctx), self, target_crs, area, options)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here create_crs_to_crs_from_pj gets an Owned Context whereas new_known_crs has a Shared Context. Is this difference deliberate?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's deliberate: the "derive from an existing Proj" methods (create_crs_to_crs_from_pj etc.) clone self's context, whereas the from-scratch constructors (new, new_known_crs) use the per-thread shared context.

I think the clone originally existed only to avoid a double-free from sharing self's raw context pointer which the ref-counted Shared context now handles anyway. The only remaining reason to clone is to inherit self's context config (search paths / network endpoint / CA bundle) into the derived transformation, which only matters when self came from a configured ProjBuilder.

The downside is that the clone opens a fresh proj.db connection, so this path doesn't get the #256 speedup. If we don't care about config inheritance here I'm happy to switch it to thread_local_context() for consistency and perf; if we do, I'll document it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the guard against the double free makes sense. Personally I tend to use new_known_crs over create_crs_to_crs_from_pj anyway because I appreciate the axis normalization that happens in new_known_crs. That this path does not get the speedup therefore would not affect me, though I can imagine it is nice have these methods more aligned in that sense. If however that breaks with the ProjBuilder path I understand some caution is warranted, especially if there are people out there count on the difference in context.

@urschrei

Copy link
Copy Markdown
Member Author

Some more (hopefully useful) context, since I wanted to make sure that we were doing it the "correct" way now:

PROJ's recommended model is one context per thread, shared by all objects created on that thread, rather than one context per object. From the official quickstart:

It is recommended to create one threading context per thread used by the program. This ensures that all PJ objects created in the same context will be sharing resources such as error-numbers and loaded grids.

This is the pattern used by GDAL (a thread-local PJ_CONTEXT in ogr_proj_p.cpp), MapServer (MapServer#7161), and pyproj since 3.7.0 (pyproj#1419). I realise that pyproj isn't "official" but it's in wide use and the authors spend a lot of time discussing their approach with the proj maintainers.

@urschrei
urschrei force-pushed the reuse_context branch 4 times, most recently from 47588ba to d5a57de Compare June 17, 2026 09:10
Proj::new and Proj::new_known_crs now borrow the per-thread shared context
instead of creating (and destroying) a fresh PROJ context each call, mirroring
pyproj's one-context-per-thread model. This drops Proj::new to
~1us per instance. ProjBuilder and the clone-based constructors continue to own
their own contexts.

Adds tests for per-thread reuse, per-thread isolation, ProjBuilder context
independence, and that dropping a Proj does not free the shared context.


Signed-off-by: Stephan Hügel <shugel@tcd.ie>
@urschrei

Copy link
Copy Markdown
Member Author

(Need someone to OK this so we can merge)

@michaelkirk michaelkirk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - I had a question, but it's just for my own understanding.

Comment thread src/lib.rs
//! ## Threading
//!
//! Each PROJ context (`PJ_CONTEXT`) is tied to a single thread and must not be used concurrently
//! from more than one thread. `Proj` is therefore deliberately neither `Send` nor `Sync`; to

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like it could be Send? But anyway, it's an improvement from what we currently have.

This behavior of using a thread-local is going to be a big speedup for pretty much everybody, but I'm wondering if there is a niche use case where you want to manage context specifically.

With the c-lib you can have multiple contexts per threads (or multiple threads per context if you're careful that they don't overlap access). You can't do that with the rust lib now, right? I think this is a minority (maybe verging on non-existent) use case, but want to make sure I'm understanding.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we could make the new design Send because of ProjContext::Shared(Rc<Context>): Rc's ref counts are non-atomic. So sending a Shared to another thread. Would keep a clone of the original Rc (or vice versa?), and that would allow the ref count to be mutated by two threads at once. That's certainly a data race, and might be UB (??).

I think we can do multiple ProjContexts per thread anyway: ProjBuilder keeps its own owned context, and clone_owned() hands a derived Proj an independent context. The thread-local sharing only applies to Proj::new and Proj::new_known_crs. So if you want a dedicated context, you can go through ProjBuilder.

Multiple threads per one context is allowed by The C lib if you serialise access yourself, but our wrapper can't enforce (even before this PR) anything that would make that safe at compile time. Maybe worth thinking about, but I haven't…

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could improve the docs with e.g. "If you need a context independent of the per-thread one, construct via ProjBuilder"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, thanks for explaining.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants