Repository: meltano/meltano

Issue:
perf: Project's `.env` contents are evaluated too many times
### Meltano Version

3.2.0

### Python Version

NA

### Bug scope

Configuration (settings parsing, validation, etc.)

### Operating System

NA

### Description

The `Project.dotenv_env` is evaluated many times per run, so a potential performance improvement would be to cache it:

https://github.com/meltano/meltano/blob/19197396dee4145b039f70210a172ef72c1f464e/src/meltano/core/project.py#L375-L382

#### Justification

See this flamegraph provided by @TyShkan: https://static.main.linendev.com/attachments/876cebd1-f3ad-485c-81a0-4b1cc971bc6a/profile.svg

#### Measuring performance

It would be nice to have https://github.com/meltano/meltano/issues/8341 in place before implementing this so we get a measure of the improvement but it's probably not a hard requirement.

### Code

_No response_


Discussion:

Resolving patch:
diff --git a/src/meltano/core/project.py b/src/meltano/core/project.py
--- a/src/meltano/core/project.py
+++ b/src/meltano/core/project.py
@@ -372,7 +372,7 @@ def dotenv(self):
         """
         return self.root.joinpath(".env")
 
-    @property
+    @cached_property
     def dotenv_env(self):
         """Get values from this project's .env file.
 


Test patch:
diff --git a/tests/meltano/core/plugin/test_plugin_settings.py b/tests/meltano/core/plugin/test_plugin_settings.py
--- a/tests/meltano/core/plugin/test_plugin_settings.py
+++ b/tests/meltano/core/plugin/test_plugin_settings.py
@@ -347,9 +347,9 @@ def test_as_env_custom(
     @pytest.mark.usefixtures("env_var")
     def test_namespace_as_env_prefix(
         self,
-        project,
+        project: Project,
         session,
-        target,
+        target: ProjectPlugin,
         plugin_settings_service_factory,
     ):
         subject = plugin_settings_service_factory(target)
@@ -376,6 +376,7 @@ def assert_env_value(value, env_var):
         # Name prefix
         dotenv.unset_key(project.dotenv, "MOCK_SCHEMA")
         dotenv.set_key(project.dotenv, "TARGET_MOCK_SCHEMA", "name_prefix")
+        project.refresh()
         assert_env_value("name_prefix", "TARGET_MOCK_SCHEMA")
 
         config = subject.as_env(session=session)
@@ -486,7 +487,7 @@ def test_store_meltano_yml(self, subject, project):
 
     @pytest.mark.order(2)
     @pytest.mark.usefixtures("tap")
-    def test_store_dotenv(self, subject, project):
+    def test_store_dotenv(self, subject: PluginSettingsService, project: Project):
         store = SettingValueStore.DOTENV
 
         assert not project.dotenv.exists()
@@ -507,6 +508,7 @@ def test_store_dotenv(self, subject, project):
         )
 
         dotenv.set_key(project.dotenv, "TAP_MOCK_BOOLEAN", "false")
+        project.refresh()
         assert subject.get_with_source("boolean") == (False, SettingValueStore.DOTENV)
         dotenv.unset_key(project.dotenv, "TAP_MOCK_BOOLEAN")
 
@@ -548,8 +550,8 @@ def test_store_dotenv(self, subject, project):
     def test_env_var_expansion(
         self,
         session,
-        subject,
-        project,
+        subject: PluginSettingsService,
+        project: Project,
         monkeypatch,
         env_var,
     ):
@@ -564,6 +566,7 @@ def test_env_var_expansion(
         dotenv.set_key(project.dotenv, "A", "rock")
         dotenv.set_key(project.dotenv, "B", "paper")
         dotenv.set_key(project.dotenv, "C", "scissors")
+        project.refresh()
 
         yml_config = {
             "var": "$VAR",
diff --git a/tests/meltano/core/test_plugin_invoker.py b/tests/meltano/core/test_plugin_invoker.py
--- a/tests/meltano/core/test_plugin_invoker.py
+++ b/tests/meltano/core/test_plugin_invoker.py
@@ -29,6 +29,7 @@ async def test_env(self, project, tap, session, plugin_invoker_factory):
         project.dotenv.touch()
         dotenv.set_key(project.dotenv, "DUMMY_ENV_VAR", "from_dotenv")
         dotenv.set_key(project.dotenv, "TAP_MOCK_TEST", "from_dotenv")
+        project.refresh()
 
         subject = plugin_invoker_factory(tap)
         async with subject.prepared(session):