Repository: feast-dev/feast

Issue:
performance issues of getting online features related to parsing protobuf data
**Is your feature request related to a problem? Please describe.**

The profiler of one application I am working on shows the `from_proto` method of the `FeatureView` class takes up 80% of the execution time. The result of cProfile is shown below.
```
    ncalls  tottime  percall  cumtime  percall filename:lineno(function)
         1    0.003    0.003   78.931   78.931 profiler.py:53(main)
       100    0.018    0.000   78.849    0.788 get_features.py:122(get_xxxx_features)
  1100/400    0.020    0.000   77.077    0.193 usage.py:274(wrapper)
 444200/9800    1.713    0.000   72.206    0.007 __init__.py:1030(wrapper)
       200    0.078    0.000   68.913    0.345 feature_store.py:1527(get_online_features)
       200    0.087    0.000   68.835    0.344 feature_store.py:1590(_get_online_features)
      3500    5.634    0.002   62.442    0.018 feature_view.py:369(from_proto)
       200    0.005    0.000   59.362    0.297 feature_store.py:2149(_get_feature_views_to_use)
       200    0.002    0.000   58.501    0.293 feature_store.py:281(_list_feature_views)
       200    0.001    0.000   58.499    0.292 registry.py:523(list_feature_views)
       200    0.016    0.000   58.495    0.292 proto_registry_utils.py:150(list_feature_views)
```
There are 3 feature views accessed by `get_xxxx_features`, however 100 calls of `get_xxxx_features` lead to 3500 calls of `from_proto`. There are 17 feature views in the feature store of this application and 3 of them are used by `get_xxxx_features`.

Environment: `continuumio/miniconda3:4.11.0` (linux/amd64) base image, `python==3.9.7`, `feast=0.31.1`, `protobuf==4.23.2`

**Describe the solution you'd like**

Instead of caching the protobuf blob of `FeatureView`, cache the `FeatureView` python object in memory.

**Describe alternatives you've considered**

modify the `get_online_features` [method](https://github.com/feast-dev/feast/blob/v0.31.1/sdk/python/feast/feature_store.py#L1528) to
```python
def get_online_features(
    self,
    features: Union[List[str], List[FeatureView], FeatureService],
    entity_rows: List[Dict[str, Any]],
    full_feature_names: bool = False,
):
``` 
so that the application developer has the option to cache the `FeatureView` objects and use them to get features directly (by passing the `get_feature_views_to_use` [step](https://github.com/feast-dev/feast/blob/v0.31.1/sdk/python/feast/feature_store.py#L1606-L1613) )
  


Discussion:
I'm facing the same issue too.

From here, ```get_feature_views_to_use``` doesn't seem like a good idea to cache this because the feature_views to use changes depending on input argument ```features```.

https://github.com/feast-dev/feast/blob/c474ccdd23ca8161de5e2958f0a12826c020dc44/sdk/python/feast/feature_store.py#L2149-L2154


What I found is, if feature registry has not expired, I assume that the ```registry.list_*``` methods always return the same result. Currently, this isn't cached, so ```list_*``` methods are being called every time, and ```from_proto``` in them is being called repeatedly and it makes ```get_online_features``` slow.
(```list_*``` methods mean ```registry.list_feature_views```, ```registry.list_data_sources``` ...)


So, my idea is to process the list_* methods using cached results when the registry is not expired.
https://github.com/feast-dev/feast/blob/c474ccdd23ca8161de5e2958f0a12826c020dc44/sdk/python/feast/infra/registry/sql.py#L244-L259
When using the sql registry, as you can see in the code above, the registry is updated by checking whether it has expired. Using this expired will tell us whether to return cached results. And I believe that caching this registry in memory would be no problem.



or caching these fvs, request_fvs, od_fvs!

https://github.com/feast-dev/feast/blob/c474ccdd23ca8161de5e2958f0a12826c020dc44/sdk/python/feast/feature_store.py#L2149-L2177

Resolving patch:
diff --git a/sdk/python/feast/infra/registry/proto_registry_utils.py b/sdk/python/feast/infra/registry/proto_registry_utils.py
--- a/sdk/python/feast/infra/registry/proto_registry_utils.py
+++ b/sdk/python/feast/infra/registry/proto_registry_utils.py
@@ -1,4 +1,5 @@
 import uuid
+from functools import wraps
 from typing import List, Optional
 
 from feast import usage
@@ -23,6 +24,26 @@
 from feast.stream_feature_view import StreamFeatureView
 
 
+def registry_proto_cache(func):
+    cache_key = None
+    cache_value = None
+
+    @wraps(func)
+    def wrapper(registry_proto: RegistryProto, project: str):
+        nonlocal cache_key, cache_value
+
+        key = tuple([id(registry_proto), registry_proto.version_id, project])
+
+        if key == cache_key:
+            return cache_value
+        else:
+            cache_value = func(registry_proto, project)
+            cache_key = key
+            return cache_value
+
+    return wrapper
+
+
 def init_project_metadata(cached_registry_proto: RegistryProto, project: str):
     new_project_uuid = f"{uuid.uuid4()}"
     usage.set_current_project_uuid(new_project_uuid)
@@ -137,8 +158,9 @@ def get_validation_reference(
     raise ValidationReferenceNotFound(name, project=project)
 
 
+@registry_proto_cache
 def list_feature_services(
-    registry_proto: RegistryProto, project: str, allow_cache: bool = False
+    registry_proto: RegistryProto, project: str
 ) -> List[FeatureService]:
     feature_services = []
     for feature_service_proto in registry_proto.feature_services:
@@ -147,6 +169,7 @@ def list_feature_services(
     return feature_services
 
 
+@registry_proto_cache
 def list_feature_views(
     registry_proto: RegistryProto, project: str
 ) -> List[FeatureView]:
@@ -157,6 +180,7 @@ def list_feature_views(
     return feature_views
 
 
+@registry_proto_cache
 def list_request_feature_views(
     registry_proto: RegistryProto, project: str
 ) -> List[RequestFeatureView]:
@@ -169,6 +193,7 @@ def list_request_feature_views(
     return feature_views
 
 
+@registry_proto_cache
 def list_stream_feature_views(
     registry_proto: RegistryProto, project: str
 ) -> List[StreamFeatureView]:
@@ -181,6 +206,7 @@ def list_stream_feature_views(
     return stream_feature_views
 
 
+@registry_proto_cache
 def list_on_demand_feature_views(
     registry_proto: RegistryProto, project: str
 ) -> List[OnDemandFeatureView]:
@@ -193,6 +219,7 @@ def list_on_demand_feature_views(
     return on_demand_feature_views
 
 
+@registry_proto_cache
 def list_entities(registry_proto: RegistryProto, project: str) -> List[Entity]:
     entities = []
     for entity_proto in registry_proto.entities:
@@ -201,6 +228,7 @@ def list_entities(registry_proto: RegistryProto, project: str) -> List[Entity]:
     return entities
 
 
+@registry_proto_cache
 def list_data_sources(registry_proto: RegistryProto, project: str) -> List[DataSource]:
     data_sources = []
     for data_source_proto in registry_proto.data_sources:
@@ -209,6 +237,7 @@ def list_data_sources(registry_proto: RegistryProto, project: str) -> List[DataS
     return data_sources
 
 
+@registry_proto_cache
 def list_saved_datasets(
     registry_proto: RegistryProto, project: str
 ) -> List[SavedDataset]:
@@ -219,6 +248,7 @@ def list_saved_datasets(
     return saved_datasets
 
 
+@registry_proto_cache
 def list_validation_references(
     registry_proto: RegistryProto, project: str
 ) -> List[ValidationReference]:
@@ -231,6 +261,7 @@ def list_validation_references(
     return validation_references
 
 
+@registry_proto_cache
 def list_project_metadata(
     registry_proto: RegistryProto, project: str
 ) -> List[ProjectMetadata]:


Test patch: