Repository: opendatacube/datacube-core

Issue:
PERF: to_epsg causes data loading to be slow for CRS without EPSG code
This issue is appearing again: #1222

After profiling the load call:
![image](https://user-images.githubusercontent.com/8699967/194952518-6f7e0364-e783-49d1-b683-4c360390515d.png)

It appears that the rasterio CRS object is being converted to a datacube CRS object about 3750 times in a single load call. The main source of the load time is the call to `to_epsg`.

I understand that there is a CRS cache to make things faster. I have been thinking about this and I and starting to think that the EPSG form will not be any better than a WKT when you are loading data from the same data source as is done in datacube.

I propose:
1. Remove `to_epsg` entirely from the cache form and rely only on `to_wkt`
2. Lazily load `to_epsg` in the CRS class.

Thoughts?

cc: @alfredoahds


Discussion:

Resolving patch:
diff --git a/datacube/drivers/postgis/_spatial.py b/datacube/drivers/postgis/_spatial.py
--- a/datacube/drivers/postgis/_spatial.py
+++ b/datacube/drivers/postgis/_spatial.py
@@ -99,7 +99,7 @@ def spindex_for_epsg(epsg: int) -> Type[SpatialIndex]:
 
 def spindex_for_crs(crs: CRS) -> Type[SpatialIndex]:
     """Return ORM class of a SpatialIndex for CRS - dynamically creating if necessary"""
-    if not (str(crs).startswith('EPSG') and crs.epsg):
+    if not str(crs).startswith("EPSG:") and crs.epsg is None:
         # Postgis identifies CRSs by a numeric "SRID" which is equivalent to EPSG number.
         _LOG.error("Cannot create a postgis spatial index for a non-EPSG-style CRS.")
         return None
diff --git a/datacube/utils/geometry/_base.py b/datacube/utils/geometry/_base.py
--- a/datacube/utils/geometry/_base.py
+++ b/datacube/utils/geometry/_base.py
@@ -116,25 +116,25 @@ def from_points(p1: Tuple[float, float],
                                    (p1[1], p2[1]))
 
 
-def _make_crs_key(crs_spec: Union[str, _CRS]) -> str:
+def _make_crs_key(crs_spec: Union[str, int, _CRS]) -> str:
     if isinstance(crs_spec, str):
         normed_epsg = crs_spec.upper()
         if normed_epsg.startswith("EPSG:"):
             return normed_epsg
         return crs_spec
+    if isinstance(crs_spec, int):
+        return f"EPSG:{crs_spec}"
     return crs_spec.to_wkt()
 
 
 @cachetools.cached({}, key=_make_crs_key)  # type: ignore[misc]
-def _make_crs(crs: Union[str, _CRS]) -> Tuple[_CRS, str, Optional[int]]:
-    if isinstance(crs, str):
-        crs = _CRS.from_user_input(crs)
-    epsg = crs.to_epsg()
-    if epsg is not None:
-        crs_str = f"EPSG:{epsg}"
-    else:
-        crs_str = crs.to_wkt()
-    return (crs, crs_str, crs.to_epsg())
+def _make_crs(crs: Union[str, int, _CRS]) -> Tuple[_CRS, str, Optional[int]]:
+    epsg = False
+    crs = _CRS.from_user_input(crs)
+    crs_str = crs.srs
+    if crs_str.upper().startswith("EPSG:"):
+        epsg = int(crs_str.split(":", maxsplit=1)[-1])
+    return (crs, crs_str, epsg)
 
 
 def _make_crs_transform_key(from_crs, to_crs, always_xy):
@@ -171,13 +171,6 @@ def __init__(self, crs_spec: Any):
         elif isinstance(crs_spec, dict):
             self._crs, self._str, self._epsg = _make_crs(_CRS.from_dict(crs_spec))
         else:
-            try:
-                epsg = crs_spec.to_epsg()
-            except AttributeError:
-                epsg = None
-            if epsg is not None:
-                self._crs, self._str, self._epsg = _make_crs(f"EPSG:{epsg}")
-                return
             try:
                 wkt = crs_spec.to_wkt()
             except AttributeError:
@@ -185,6 +178,13 @@ def __init__(self, crs_spec: Any):
             if wkt is not None:
                 self._crs, self._str, self._epsg = _make_crs(wkt)
                 return
+            try:
+                epsg = crs_spec.to_epsg()
+            except AttributeError:
+                epsg = None
+            if epsg is not None:
+                self._crs, self._str, self._epsg = _make_crs(epsg)
+                return
 
             raise CRSError(
                 "Expect string or any object with `.to_epsg()` or `.to_wkt()` methods"
@@ -213,11 +213,14 @@ def to_epsg(self) -> Optional[int]:
         """
         EPSG Code of the CRS or None
         """
+        if self._epsg is not False:
+            return self._epsg
+        self._epsg = self._crs.to_epsg()
         return self._epsg
 
     @property
     def epsg(self) -> Optional[int]:
-        return self._epsg
+        return self.to_epsg()
 
     @property
     def semi_major_axis(self):


Test patch: