Opened 3 weeks ago

Closed 14 hours ago

#36960 closed Cleanup/optimization (fixed)

Django never uses psycopg 3's optimised timestamptzloader

Reported by: Aarni Koskela Owned by: Aarni Koskela
Component: Database layer (models, ORM) Version: 6.0
Severity: Normal Keywords: postgresql
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

When available (e.g. psycopg3-c or psycopg3-binary installed), Psycopg implicitly registers a faster TimestamptzLoader implementation https://github.com/psycopg/psycopg/blob/master/psycopg_c/psycopg_c/types/datetime.pyx.

However, Django overrides this with a custom class that derives from the Python psycopg.types.datetime.TimestamptzLoader implementation. (You can't derive from the final-marked C speedup class, anyway.)

For performance, it would be pleasant if Django used the faster class when available. This is made slightly hairy by the fact that, as mentioned, you can't use the speedup class as a base, and there is no public API in psycopg to get the speedup class. context.adapters._get_optimised exists, but is not a public API. You could do something like (and I experimentally did)

                try:
                    optimised_class = context.adapters._get_optimised(TimestamptzLoader)
                    if optimised_class is not TimestamptzLoader:
                        self.fast_load = optimised_class(oid, context).load
                except Exception:
                    pass

and res = (self.fast_load or super().load)(data) but that's also hairy...

Change History (8)

comment:1 by Jacob Walls, 3 weeks ago

Resolution: wontfix
Status: newclosed
Type: BugCleanup/optimization

Thanks for the idea. I played with the provided PR and added some timeit statements, and found about a 4x performance improvement. However, I don't think the provided solution is adequate, since, as you acknowledge, there is no public interface for this.

We could revisit with one of two things in hand:

  • a public API from psycopg for retrieving the c-accelerated internals (unlikely...)
  • a Django-side solution where instead of subclassing the loaders, we can patch the load() method dynamically like this, although the cursor timezone registration needs investigation:
  • django/db/backends/postgresql/base.py

    diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py
    index 42b37ab3c2..b4fe84be65 100644
    a b class DatabaseWrapper(BaseDatabaseWrapper):  
    441441            # Register the cursor timezone only if the connection disagrees, to
    442442            # avoid copying the adapter map.
    443443            tzloader = self.connection.adapters.get_loader(TIMESTAMPTZ_OID, Format.TEXT)
     444            # This fails, needs investigation.
    444445            if self.timezone != tzloader.timezone:
    445446                register_tzloader(self.timezone, cursor)
    446447        else:
  • django/db/backends/postgresql/psycopg_any.py

    diff --git a/django/db/backends/postgresql/psycopg_any.py b/django/db/backends/postgresql/psycopg_any.py
    index dea4800fce..221c18c6f4 100644
    a b try:  
    2222            return ClientCursor(cursor.connection).mogrify(sql, params)
    2323
    2424    # Adapters.
    25     class BaseTzLoader(TimestamptzLoader):
    26         """
    27         Load a PostgreSQL timestamptz using the a specific timezone.
    28         The timezone can be None too, in which case it will be chopped.
    29         """
    30 
    31         timezone = None
    32 
    33         def load(self, data):
    34             res = super().load(data)
    35             return res.replace(tzinfo=self.timezone)
    36 
    3725    def register_tzloader(tz, context):
    38         class SpecificTzLoader(BaseTzLoader):
    39             timezone = tz
    40 
    41         context.adapters.register_loader("timestamptz", SpecificTzLoader)
     26        TimestamptzLoader.load = lambda self, data: TimestamptzLoader.load(self, data).replace(tz)
     27        context.adapters.register_loader("timestamptz", TimestamptzLoader)
    4228
    4329    class DjangoRangeDumper(RangeDumper):
    4430        """A Range dumper customized for Django."""

I don't know how practical that sketched idea would be. It's not very great python, either, so even then I'm still uncertain.

comment:2 by Aarni Koskela, 3 weeks ago

a public API from psycopg for retrieving the c-accelerated internals (unlikely...)

I'm not sure that's totally implausible; the author/maintainer of psycopg3, @dvarrazzo-on-GitHub, chimed in on the original issue porting his psycopg3 backend to Django in https://github.com/django/django/pull/15687#issuecomment-1348923713, and is responsive on the psycopg repo (I've been looking at some perf fixes on that side of the fence.)

I could make a PR there to propose making get_optimised a public API, and link this thread.

comment:3 by Jacob Walls, 3 weeks ago

Resolution: wontfix
Status: closednew
Triage Stage: UnreviewedSomeday/Maybe

Great plan. I'll signal that by moving this to Someday/Maybe to represent we're waiting on upstream input.

comment:4 by Aarni Koskela, 3 weeks ago

:+1: Opened an issue for discussion for now instead of going full in with another PR: https://github.com/psycopg/psycopg/issues/1273

comment:5 by Aarni Koskela, 6 days ago

Has patch: set
Owner: set to Aarni Koskela
Status: newassigned

comment:6 by Jacob Walls, 6 days ago

Triage Stage: Someday/MaybeAccepted

Accepting, now that we have guidance from psycopg about the proper way to do this.

PR

comment:7 by Jacob Walls, 14 hours ago

Triage Stage: AcceptedReady for checkin

comment:8 by Jacob Walls <jacobtylerwalls@…>, 14 hours ago

Resolution: fixed
Status: assignedclosed

In 994fa77:

Fixed #36960 -- Enabled the use of psycopg 3's optimized timestamp loader.

Based on Daniele Varrazzo's comment in https://github.com/psycopg/psycopg/issues/1273#issuecomment-3986829769

Note: See TracTickets for help on using tickets.
Back to Top