Repository: dimagi/commcare-hq
Pull request #35830: Profile some of the CLE report's methods
Author: login: Charl1996
Labels: name: product/all-users-all-environments
description: Change impacts all users on all environments
name: product/invisible
description: Change has no end-user visible impact
Created: 2025-02-24T10:11:50Z | Merged: 2025-03-05T06:20:09Z

Note: This PR is the second in a series to capture timing information around the CLE processing.

## Technical Summary
[Ticket](https://dimagi.atlassian.net/browse/SC-4180)

This ticket makes use of the `ESQueryProfilerMixin` by adding it to the `CaseListMixin` and augmenting some methods of the `CaseListExplorer` to capture timing information around the processing of report rows.

A new decorator, `profile`, has been created to make it easier (and less verbose) to make use of the profiler for capturing a report method's timing information. 

Below is a screenshot of the raw json of what the profiling data looks like (example taken from running on local machine). Pay attention to the nesting and what each nested block reports. 

![image](https://github.com/user-attachments/assets/1b4506f9-6e3c-4ba1-b9d6-b95e7402993f)


## Safety Assurance

### Safety story
Tested locally

### Automated test coverage
Appended one-liner to test. Don't think much more is really required at this stage. 

### QA Plan
QA to commence as part of [this PR](https://github.com/dimagi/commcare-hq/pull/35854)

### Rollback instructions
- [x] This PR can be reverted after deploy with no further considerations

### Labels & Review
- [x] Risk label is set correctly
- [x] The set of people pinged as reviewers is appropriate for the level of risk of the change



Diff:
diff --git a/corehq/apps/reports/standard/__init__.py b/corehq/apps/reports/standard/__init__.py
index 1d921eed9ef9..b5bd32d903a3 100644
--- a/corehq/apps/reports/standard/__init__.py
+++ b/corehq/apps/reports/standard/__init__.py
@@ -1,5 +1,6 @@
 import warnings
 from datetime import datetime
+from functools import wraps
 
 from django.core.cache import cache
 from django.urls import reverse
@@ -309,8 +310,32 @@ def year(self):
 
 
 class ESQueryProfilerMixin(object):
-    """
-    This mixin makes it possible to profile your ES queries.
+    """Mixin for profiling Elasticsearch queries.
+
+    This mixin provides timing and profiling capabilities methods in report classes.
+
+    Attributes:
+        profiler_enabled (bool): Must be set to True to enable profiling
+        profiler_name (str): Name of the report to be used in profiling output
+
+    Usage:
+        There are two ways to capture timing information:
+
+        1. Using the decorator:
+            >>> from corehq.apps.reports.standard import profile
+            >>> class MyReport(...):
+            ...     @profile('Method Name')
+            ...     def my_method(self):
+            ...         pass
+
+        2. Using the context manager:
+            >>> def my_method(self):
+            ...     with self.profiler.timing_context('Method Name'):
+            ...         pass
+
+    Notes:
+        The appropriate report class must have profiler_enabled and profiler_name
+        attributes set for the profiling functionality to work.
     """
     profiler_enabled = False
     profiler_name = 'Case List'
@@ -334,3 +359,19 @@ def _get_search_class(self):
         if not self.search_class:
             raise NotImplementedError("You must define a search_class attribute.")
         return self.search_class
+
+
+def profile(name=None):
+    """
+    This decorator wraps the given function with a timing context. The results will
+    be labeled by `name`.
+    """
+    def decorator(func):
+        @wraps(func)
+        def wrapper(obj, *args, **kwargs):
+            if obj.profiler_enabled:
+                with obj.profiler.timing_context(name):
+                    return func(obj, *args, **kwargs)
+            return func(obj, *args, **kwargs)
+        return wrapper
+    return decorator
diff --git a/corehq/apps/reports/standard/cases/basic.py b/corehq/apps/reports/standard/cases/basic.py
index 90776070cae4..2bc93f43a518 100644
--- a/corehq/apps/reports/standard/cases/basic.py
+++ b/corehq/apps/reports/standard/cases/basic.py
@@ -1,3 +1,5 @@
+import contextlib
+
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext_lazy
 
@@ -12,8 +14,10 @@
 from corehq.apps.reports.filters.select import SelectOpenCloseFilter
 from corehq.apps.reports.generic import ElasticProjectInspectionReport
 from corehq.apps.reports.standard import (
+    ESQueryProfilerMixin,
     ProjectReport,
     ProjectReportParametersMixin,
+    profile,
 )
 from corehq.apps.reports.standard.cases.filters import CaseSearchFilter
 from corehq.apps.reports.standard.cases.utils import (
@@ -28,7 +32,7 @@
 from .data_sources import CaseDisplayES
 
 
-class CaseListMixin(ElasticProjectInspectionReport, ProjectReportParametersMixin):
+class CaseListMixin(ESQueryProfilerMixin, ElasticProjectInspectionReport, ProjectReportParametersMixin):
     fields = [
         'corehq.apps.reports.filters.case_list.CaseListFilter',
         'corehq.apps.reports.filters.select.CaseTypeFilter',
@@ -107,7 +111,7 @@ def _build_query(self):
     @memoized
     def es_results(self):
         try:
-            return self._build_query().run().raw
+            return self._run_es_query()
         except ESError as e:
             original_exception = e.args[0]
             if isinstance(original_exception, TransportError):
@@ -116,6 +120,10 @@ def es_results(self):
                         raise BadRequestError()
             raise e
 
+    @profile("ES query")
+    def _run_es_query(self):
+        return self._build_query().run().raw
+
     @property
     @memoized
     def case_owners(self):
@@ -234,3 +242,13 @@ def rows(self):
                 display.modified_on,
                 display.closed_display
             ]
+
+    @property
+    def json_response(self):
+        with self.profiler.timing_context if self.profiler_enabled else contextlib.nullcontext():
+            response = super().json_response
+
+        if self.profiler_enabled:
+            # Todo: SC-4181
+            pass
+        return response
diff --git a/corehq/apps/reports/standard/cases/case_list_explorer.py b/corehq/apps/reports/standard/cases/case_list_explorer.py
index 428d61ae4527..56d15527942c 100644
--- a/corehq/apps/reports/standard/cases/case_list_explorer.py
+++ b/corehq/apps/reports/standard/cases/case_list_explorer.py
@@ -20,6 +20,7 @@
     CaseTypeFilter,
     SelectOpenCloseFilter,
 )
+from corehq.apps.reports.standard import profile
 from corehq.apps.reports.standard.cases.basic import CaseListReport
 from corehq.apps.reports.standard.cases.data_sources import SafeCaseDisplay
 from corehq.apps.reports.standard.cases.filters import (
@@ -62,6 +63,9 @@ class CaseListExplorer(CaseListReport, XpathCaseSearchFilterMixin):
     documentation_link = DOCS_LINK_CASE_LIST_EXPLORER
     use_bootstrap5 = False
 
+    profiler_enabled = True
+    profiler_name = "Case List Explorer"
+
     exportable = True
     exportable_all = True
     emailable = True
@@ -93,9 +97,10 @@ def es_results(self):
     def _build_query(self, sort=True):
         query = super(CaseListExplorer, self)._build_query()
         query = self._populate_sort(query, sort)
-        query = self.apply_xpath_case_search_filter(query)
+        query = self._apply_xpath_case_search_filter(query)
         return query
 
+    @profile("Populate sort")
     def _populate_sort(self, query, sort):
         if not sort:
             # Don't sort on export
@@ -119,6 +124,10 @@ def _populate_sort(self, query, sort):
                 query = query.sort_by_case_property(column.prop_name, desc=descending)
         return query
 
+    @profile("Apply Xpath case filter")
+    def _apply_xpath_case_search_filter(self, query):
+        return self.apply_xpath_case_search_filter(query)
+
     @property
     def columns(self):
         view_case_column = DataTablesColumn(
@@ -171,6 +180,7 @@ def headers(self):
         return headers
 
     @property
+    @profile("Retrieving rows")
     def rows(self):
         self.track_search()
         data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
@@ -194,6 +204,7 @@ def get_all_rows(self):
         data = (wrap_case_search_hit(r) for r in query.scroll_ids_to_disk_and_iter_docs())
         return self._get_rows(data)
 
+    @profile("Preparing data for display")
     def _get_rows(self, data):
         timer = metrics_histogram_timer(
             'commcare.case_list_explorer_query.row_fetch_timings',
diff --git a/corehq/apps/reports/tests/test_case_list_explorer_report.py b/corehq/apps/reports/tests/test_case_list_explorer_report.py
index c8a6f99bc247..0f95d7cee8c4 100644
--- a/corehq/apps/reports/tests/test_case_list_explorer_report.py
+++ b/corehq/apps/reports/tests/test_case_list_explorer_report.py
@@ -73,6 +73,8 @@ def test_with_explorer_columns(self):
         self.request.GET = get_query_dict
         cle = CaseListExplorer(self.request, domain=self.domain_name)
 
+        assert cle.profiler_enabled
+
         header_names = []
         header_prop_names = []
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>corehq/apps/reports/tests/test_case_list_explorer_report.py (2)</summary><blockquote>

`87-108`: **Improve property access and approve test implementation.**

The test implementation correctly verifies the profiling functionality. However, there's a minor improvement needed in how the property is accessed.


Apply this diff to improve the property access:

```diff
-        cle.json_response
+        _ = cle.json_response
```

This change explicitly shows that we're intentionally accessing the property for its side effects.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

106-106: Found useless expression. Either assign it to a variable or remove it.

(B018)

</details>

</details>

---

`102-107`: **Enhance test assertions and fix property access.**

The assertions verify the profiler state correctly, but there are a few improvements to consider:
1. Add descriptive messages to assertions for better test failure diagnostics
2. Fix the property access on line 106 which is currently not assigned or used

Apply this diff to improve the test:

```diff
-        assert cle.profiler_enabled
-        assert cle.profiler.timing_context.to_dict()['percent_total'] is None
+        assert cle.profiler_enabled, "Profiler should be enabled for CaseListExplorer"
+        assert cle.profiler.timing_context.to_dict()['percent_total'] is None, "Initial percent_total should be None"

         # Run the CLE
-        cle.json_response
-        assert cle.profiler.timing_context.to_dict()['percent_total'] == 100.0
+        _ = cle.json_response  # Access the property to trigger profiling
+        assert cle.profiler.timing_context.to_dict()['percent_total'] == 100.0, "Final percent_total should be 100.0"
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

106-106: Found useless expression. Either assign it to a variable or remove it.

(B018)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yaml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e1fac070dd5da3ddddb542da971210ca038a2535 and e680cf0eaa26758eae15b5e9120796d2cff0c1b7.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `corehq/apps/reports/standard/__init__.py` (3 hunks)
* `corehq/apps/reports/standard/cases/basic.py` (5 hunks)
* `corehq/apps/reports/standard/cases/case_list_explorer.py` (6 hunks)
* `corehq/apps/reports/tests/test_case_list_explorer_report.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (1)</summary>

<details>
<summary>`**/*.py`: - Review the code following best practises and st...</summary>

> `**/*.py`: - Review the code following best practises and standards

- `corehq/apps/reports/tests/test_case_list_explorer_report.py`
- `corehq/apps/reports/standard/__init__.py`
- `corehq/apps/reports/standard/cases/basic.py`
- `corehq/apps/reports/standard/cases/case_list_explorer.py`

</details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>corehq/apps/reports/tests/test_case_list_explorer_report.py</summary>

106-106: Found useless expression. Either assign it to a variable or remove it.

(B018)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>corehq/apps/reports/standard/__init__.py (1)</summary>

`313-377`: **Well-implemented profiling decorator with comprehensive documentation!**

The implementation is clean and follows best practices:
- Proper use of `wraps` to preserve function metadata
- Elegant fallback to `nullcontext()` for when profiling is disabled
- Clear and detailed documentation with usage examples

</details>
<details>
<summary>corehq/apps/reports/standard/cases/case_list_explorer.py (2)</summary>

`66-67`: **Profiler configuration looks good!**

The profiler is properly configured with appropriate values for `profiler_enabled` and `profiler_name`.

---

`127-130`: **Good separation of concerns for profiling!**

Extracting `_apply_xpath_case_search_filter` into a separate method allows for more granular profiling of the XPath case filter application.

</details>
<details>
<summary>corehq/apps/reports/tests/test_case_list_explorer_report.py (1)</summary>

`87-101`: **LGTM! Well-structured test setup.**

The test setup follows consistent patterns with other test methods in the class and properly sets up the required test data.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-24T13:09:43Z
user: login: esoergel
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-25T20:39:24Z
user: login: Charl1996
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T09:46:34Z
user: login: mkangia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T08:43:26Z
user: login: mkangia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T08:45:00Z
user: login: mkangia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T08:46:39Z
user: login: mkangia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T08:47:25Z
user: login: mkangia
body: Looking good.

Can you share a screenshot or two in the description on how this would look?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T08:49:18Z
user: login: Charl1996
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T13:32:41Z
user: login: Charl1996
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T13:35:02Z
user: login: Charl1996
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-27T13:49:29Z
user: login: zandre-eng
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T11:37:28Z
user: login: mkangia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:08:42Z
user: login: mkangia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:09:57Z
user: login: Charl1996
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:11:11Z
user: login: Charl1996
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:12:47Z
user: login: mkangia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:14:26Z
user: login: mkangia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:16:00Z
user: login: mkangia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:26:43Z
user: login: mkangia
body: Looks good to me.

As a follow up, you could consider how the metrics how up on admin restore page. just a better UI experience than a JSON.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:27:54Z
user: login: Charl1996
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:40:56Z
user: login: Charl1996
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:41:10Z
user: login: mkangia
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:53:24Z

Review comments:
diff_hunk: @@ -234,3 +237,13 @@ def rows(self):
                 display.modified_on,
                 display.closed_display
             ]
+
+    @property
+    def json_response(self):
+        with self.profiler.timing_context if self.profiler_enabled else contextlib.nullcontext():
+            response = super().json_response
+
+        if self.profiler_enabled:
+            # Todo: SC-4181
+            pass
+        return response
path: corehq/apps/reports/standard/cases/basic.py
user: login: coderabbitai[bot]
body: _:bulb: Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify tracking of the TODO comment.**

The TODO comment references ticket SC-4181. Let's ensure this is being tracked.

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Check if the TODO is tracked in an issue

gh issue view SC-4181 2>/dev/null || echo "Issue SC-4181 not found"
```

Length of output: 80

---



</details>

**ACTION REQUIRED: Update or Create Tracking for TODO**

The TODO comment in `corehq/apps/reports/standard/cases/basic.py` (lines 241-249) references ticket **SC-4181**, but our check indicates that this issue does not exist. Please either create a GitHub issue for SC-4181 to properly track this work, update the comment to reference an existing valid ticket, or remove the TODO comment if it’s no longer applicable.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-02-24T13:09:43Z
author_association: NONE
diff_hunk: @@ -309,8 +311,32 @@ def year(self):
 
 
 class ESQueryProfilerMixin(object):
-    """
-    This mixin makes it possible to profile your ES queries.
+    """Mixin for profiling Elasticsearch queries.
path: corehq/apps/reports/standard/__init__.py
user: login: esoergel
body: Do you intend to make it possible run the elasticsearch query profiler in here too?  (ie, setting `debug_mode=True` in `ESQueryProfiler`)

Sounds potentially useful for digging in deeper on certain queries, just want to flag that as per [the docs](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-profile.html) it's not suitable for production use (so it'd probably have to be superuser only or something)
created_at: 2025-02-25T20:39:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -309,8 +311,32 @@ def year(self):
 
 
 class ESQueryProfilerMixin(object):
-    """
-    This mixin makes it possible to profile your ES queries.
+    """Mixin for profiling Elasticsearch queries.
path: corehq/apps/reports/standard/__init__.py
user: login: Charl1996
body: > Do you intend to make it possible run the elasticsearch query profiler in here too?

Yes, but I'll do that as part of [this ticket](https://dimagi.atlassian.net/browse/SC-4182).
created_at: 2025-02-26T09:46:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,8 +182,11 @@ def headers(self):
     @property
     def rows(self):
         self.track_search()
-        data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
-        return self._get_rows(data)
+
+        with self.profiler.timing_context("Retrieving rows"):
+            data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
path: corehq/apps/reports/standard/cases/case_list_explorer.py
user: login: mkangia
body: you could consider tracking "Fetching results from ES" and "Wrapping result" as two different metrics, just to be very specific about how long ES took.
created_at: 2025-02-27T08:43:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,8 +182,11 @@ def headers(self):
     @property
     def rows(self):
         self.track_search()
-        data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
-        return self._get_rows(data)
+
+        with self.profiler.timing_context("Retrieving rows"):
+            data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
+            with self.profiler.timing_context("Parsing rows"):
path: corehq/apps/reports/standard/cases/case_list_explorer.py
user: login: mkangia
body: nit: preparing data for display?
created_at: 2025-02-27T08:45:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -334,3 +336,18 @@ def _get_search_class(self):
         if not self.search_class:
             raise NotImplementedError("You must define a search_class attribute.")
         return self.search_class
+
+
+def profile(name=None):
path: corehq/apps/reports/standard/__init__.py
user: login: mkangia
body: ooo very nice!
created_at: 2025-02-27T08:46:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -180,13 +180,11 @@ def headers(self):
         return headers
 
     @property
+    @profile("Retrieving rows")
     def rows(self):
         self.track_search()
-
-        with self.profiler.timing_context("Retrieving rows"):
-            data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
-            with self.profiler.timing_context("Parsing rows"):
-                return self._get_rows(data)
+        data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
+        return self._get_rows(data)
path: corehq/apps/reports/standard/cases/case_list_explorer.py
user: login: mkangia
body: nit: I liked the specific tracking before. There is a lot happening here to put under one metric.
created_at: 2025-02-27T08:47:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,8 +182,11 @@ def headers(self):
     @property
     def rows(self):
         self.track_search()
-        data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
-        return self._get_rows(data)
+
+        with self.profiler.timing_context("Retrieving rows"):
+            data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
+            with self.profiler.timing_context("Parsing rows"):
path: corehq/apps/reports/standard/cases/case_list_explorer.py
user: login: Charl1996
body: I like that. I'll update.
created_at: 2025-02-27T13:32:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -180,13 +180,11 @@ def headers(self):
         return headers
 
     @property
+    @profile("Retrieving rows")
     def rows(self):
         self.track_search()
-
-        with self.profiler.timing_context("Retrieving rows"):
-            data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
-            with self.profiler.timing_context("Parsing rows"):
-                return self._get_rows(data)
+        data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
+        return self._get_rows(data)
path: corehq/apps/reports/standard/cases/case_list_explorer.py
user: login: Charl1996
body: I'll add a screenshot so you can see how the different timings are being reported.
created_at: 2025-02-27T13:35:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,8 +182,11 @@ def headers(self):
     @property
     def rows(self):
         self.track_search()
-        data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
-        return self._get_rows(data)
+
+        with self.profiler.timing_context("Retrieving rows"):
+            data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
+            with self.profiler.timing_context("Parsing rows"):
path: corehq/apps/reports/standard/cases/case_list_explorer.py
user: login: Charl1996
body: https://github.com/dimagi/commcare-hq/pull/35830/commits/8451b535f2f516a16950738006212279e5c5ff42
created_at: 2025-02-27T13:49:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -180,13 +180,11 @@ def headers(self):
         return headers
 
     @property
+    @profile("Retrieving rows")
     def rows(self):
         self.track_search()
-
-        with self.profiler.timing_context("Retrieving rows"):
-            data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
-            with self.profiler.timing_context("Parsing rows"):
-                return self._get_rows(data)
+        data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
+        return self._get_rows(data)
path: corehq/apps/reports/standard/cases/case_list_explorer.py
user: login: mkangia
body: so, the "wrap_case_search_hit" and the time taken by ES to get the data is clubbed into "Retrieving rows" right?
created_at: 2025-03-04T12:08:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,7 +15,7 @@
 from corehq.apps.reports.generic import ElasticProjectInspectionReport
 from corehq.apps.reports.standard import (
     ProjectReport,
-    ProjectReportParametersMixin,
+    ProjectReportParametersMixin, ESQueryProfilerMixin,
path: corehq/apps/reports/standard/cases/basic.py
user: login: mkangia
body: nit: isort.
created_at: 2025-03-04T12:09:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -180,13 +180,11 @@ def headers(self):
         return headers
 
     @property
+    @profile("Retrieving rows")
     def rows(self):
         self.track_search()
-
-        with self.profiler.timing_context("Retrieving rows"):
-            data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
-            with self.profiler.timing_context("Parsing rows"):
-                return self._get_rows(data)
+        data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
+        return self._get_rows(data)
path: corehq/apps/reports/standard/cases/case_list_explorer.py
user: login: Charl1996
body: Yes. Feel free to suggest different wording if you think it's necessary.
created_at: 2025-03-04T12:11:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,7 +15,7 @@
 from corehq.apps.reports.generic import ElasticProjectInspectionReport
 from corehq.apps.reports.standard import (
     ProjectReport,
-    ProjectReportParametersMixin,
+    ProjectReportParametersMixin, ESQueryProfilerMixin,
path: corehq/apps/reports/standard/cases/basic.py
user: login: Charl1996
body: https://github.com/dimagi/commcare-hq/pull/35830/commits/988e0473119ee24e92f7fb5ad085963e6719b225
created_at: 2025-03-04T12:12:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -334,3 +360,18 @@ def _get_search_class(self):
         if not self.search_class:
             raise NotImplementedError("You must define a search_class attribute.")
         return self.search_class
+
+
+def profile(name=None):
+    """
+    This decorator wraps the given function with a timing context. The results will
+    be labeled by `name`.
+    """
+    def decorator(func):
+        @wraps(func)
+        def wrapper(obj, *args, **kwargs):
+            with obj.profiler.timing_context(name) if obj.profiler_enabled else contextlib.nullcontext():
+                result = func(obj, *args, **kwargs)
+            return result
path: corehq/apps/reports/standard/__init__.py
user: login: mkangia
body: nit: is it simpler with?
```
if obj.profiler_enabled:
    with obj.profiler.timing_context(name):
        return func(obj, *args, **kwargs)
else:
   return func(obj, *args, **kwargs)
```
created_at: 2025-03-04T12:14:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -107,7 +109,8 @@ def _build_query(self):
     @memoized
     def es_results(self):
         try:
-            return self._build_query().run().raw
+            with self.profiler.timing_context("ES query") if self.profiler_enabled else contextlib.nullcontext():
+                return self._build_query().run().raw
path: corehq/apps/reports/standard/cases/basic.py
user: login: mkangia
body: nit: for sake of code clarity can this be extracted in a method and decorated with profile as well?
created_at: 2025-03-04T12:16:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -180,13 +180,11 @@ def headers(self):
         return headers
 
     @property
+    @profile("Retrieving rows")
     def rows(self):
         self.track_search()
-
-        with self.profiler.timing_context("Retrieving rows"):
-            data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
-            with self.profiler.timing_context("Parsing rows"):
-                return self._get_rows(data)
+        data = (wrap_case_search_hit(row) for row in self.es_results['hits'].get('hits', []))
+        return self._get_rows(data)
path: corehq/apps/reports/standard/cases/case_list_explorer.py
user: login: mkangia
body: Feel free to ignore this is this is drilling to deep for metrics.
I was just thinking of
```
@profile("Retrieving rows")
def rows(self):
    self.track_search()
    
    es_hits = self.es_results()
    data = self._wrap_results(es_hits)
    return self._get_rows(data)
    
@profile("Fetching Elasticsearch hits")
def es_results(self):
    #...

@profile("Wrap Elasticsearch hits")
def _wrap_results(es_hits):
    return [wrap_case_search_hit(row) for row in es_hits]

def _get_rows():
    #...
```
created_at: 2025-03-04T12:26:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -334,3 +360,18 @@ def _get_search_class(self):
         if not self.search_class:
             raise NotImplementedError("You must define a search_class attribute.")
         return self.search_class
+
+
+def profile(name=None):
+    """
+    This decorator wraps the given function with a timing context. The results will
+    be labeled by `name`.
+    """
+    def decorator(func):
+        @wraps(func)
+        def wrapper(obj, *args, **kwargs):
+            with obj.profiler.timing_context(name) if obj.profiler_enabled else contextlib.nullcontext():
+                result = func(obj, *args, **kwargs)
+            return result
path: corehq/apps/reports/standard/__init__.py
user: login: Charl1996
body: https://github.com/dimagi/commcare-hq/pull/35830/commits/b2629164fb2bbc42a16e7c6f39db3de459fdc5f8
created_at: 2025-03-04T12:40:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -107,7 +109,8 @@ def _build_query(self):
     @memoized
     def es_results(self):
         try:
-            return self._build_query().run().raw
+            with self.profiler.timing_context("ES query") if self.profiler_enabled else contextlib.nullcontext():
+                return self._build_query().run().raw
path: corehq/apps/reports/standard/cases/basic.py
user: login: Charl1996
body: https://github.com/dimagi/commcare-hq/pull/35830/commits/58576de8e14eb4297cba678c6e751357a2efc3a3
created_at: 2025-03-04T12:41:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: Charl1996
created_at: 2025-02-24T13:06:52Z
body: @coderabbit please review
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-02-24T13:07:35Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>🧩 Analysis chain</summary>

@Charl1996 I'll review the changes in your PR that adds profiling capabilities to the CLE report methods.



---

🏁 Script executed:
```
#!/bin/bash
# Search for relevant files
echo "Files containing CaseListMixin:"
rg "class CaseListMixin" -l

echo -e "\nFiles containing ESQueryProfilerMixin:"
rg "class ESQueryProfilerMixin" -l

echo -e "\nFiles containing new profile decorator:"
rg "def profile" -A 5
```

Length of output: 6291

---

🏁 Script executed:
```
#!/bin/bash
# Check the profile decorator implementation
echo "Profile decorator implementation:"
cat corehq/apps/reports/standard/__init__.py

echo -e "\nCaseListMixin implementation:"
cat corehq/apps/reports/standard/cases/basic.py

# Look for tests
echo -e "\nRelated test files:"
fd "test.*case.*list" -t f
```

Length of output: 21631

---



</details>

@Charl1996 I've reviewed the changes that add profiling capabilities to the CLE report methods. The implementation looks solid with:

1. A clean profile decorator implementation that makes it easy to add timing to methods
2. Proper integration of ESQueryProfilerMixin into CaseListMixin
3. Timing context for ES queries
4. Existing test coverage in place

One minor note: There's a TODO comment (SC-4181) in the `json_response` method that should be addressed in the next ticket.

The changes look good and align well with the PR's objective of capturing timing information around the CLE processing. The implementation is clean and non-intrusive.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Just%20received%20an%20insightful%20review%20from%20%40coderabbitai!%20%F0%9F%9A%80%20Their%20feedback%20on%20optimizing%20our%20code%20structure%20will%20enhance%20performance%20and%20maintainability.%20Highly%20recommend%20trying%20this%20tool!%20%F0%9F%92%A1&url=https%3A%2F%2Fgithub.com%2Fdimagi%2Fcommcare-hq%2Fpull%2F35830)?
							Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->

<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-02-24T13:09:40Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This pull request introduces a new `profile` decorator that wraps method calls in a timing context if profiling is enabled. The decorator is designed to work conditionally, utilizing the profiler’s `timing_context` when the `profiler_enabled` attribute is set to `True`, or defaulting to a no-operation context otherwise. Several report classes, including those derived from `ESQueryProfilerMixin`, now leverage this decorator to capture performance metrics during query building, sorting, row retrieval, and JSON responses. Additionally, the documentation for the `ESQueryProfilerMixin` has been expanded to clarify its usage and attributes. A new test in the explorer report suite verifies that profiling correctly tracks performance metrics during report execution. Overall, these changes enhance the observability of Elasticsearch query performance without altering the core business logic.

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Caller
    participant DecoratedMethod as Method (with @profile)
    participant Profiler

    Caller ->> DecoratedMethod: Call profiled method
    alt Profiling enabled
        DecoratedMethod ->> Profiler: Enter timing_context(profiler_name)
        Profiler -->> DecoratedMethod: Timing context entered
        DecoratedMethod ->> DecoratedMethod: Execute method logic
        DecoratedMethod ->> Profiler: Exit timing context (record timing)
    else Profiling disabled
        DecoratedMethod ->> DecoratedMethod: Use nullcontext (no profiling)
        DecoratedMethod ->> DecoratedMethod: Execute method logic
    end
```

## Possibly related PRs

- **dimagi/commcare-hq#35743**: Introduces similar profiling features using `ESQueryProfilerMixin` and enhances query profiling in related report classes.

## Suggested labels

`product/invisible`

## Suggested reviewers

- kaapstorm
- orangejenny
- mkangia
- AmitPhulera
- stephherbers
- Jtang-1

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings (Beta)

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR. ([Beta](https://docs.coderabbit.ai/early-access#beta))
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: Charl1996
created_at: 2025-02-24T15:10:12Z
body: @coderabbit resolve
author_association: CONTRIBUTOR
user: login: Charl1996
created_at: 2025-02-27T13:45:43Z
body: > Can you share a screenshot or two in the description on how this would look?

@mkangia See updated description.

author_association: CONTRIBUTOR
user: login: Charl1996
created_at: 2025-02-27T14:35:21Z
body: Now that I'm looking at the timings again more closely I think there's still some room to add more profiling, since the "ES Query" part only makes up about 15% of the "Retrieving rows", which in turn makes up 96% of the total (for that test run). Maybe I'll run it a couple of times more and see if I can capture the other parts.
author_association: CONTRIBUTOR
user: login: Charl1996
created_at: 2025-03-03T09:30:10Z
body: Actually, on a bunch of subsequent runs locally it seem like the execution of the query takes the majority of the time of the "ES query" block, which in turn makes up > 80% of the "Retrieving rows" block, so I think the current profiling "checkpoints" is good enough.
author_association: CONTRIBUTOR
user: login: sentry[bot]
created_at: 2025-03-06T07:11:12Z
body: ## Suspect Issues
This pull request was deployed and Sentry observed the following issues:

- ‼️ **AttributeError: 'NoneType' object has no attribute 'timing_context'** `corehq.apps.saved_reports.tasks.send_email_report` [View Issue](https://dimagi.sentry.io/issues/6362523375/?referrer=github-pr-bot)
- ‼️ **ESError: TransportError(503, 'cluster_block_exception', 'blocked by: [SERVICE_UNAVAILABLE/1/state not recovered / initialized];')** `/a/{domain}/reports/{var}|{var}|{var}|{var}|{va...` [View Issue](https://dimagi.sentry.io/issues/6362525730/?referrer=github-pr-bot)
- ‼️ **ESError: TransportError(503, 'cluster_block_exception', 'blocked by: [SERVICE_UNAVAILABLE/1/state not recovered / initialized];')** `/a/{domain}/reports/{var}|{var}|{var}|{var}|{va...` [View Issue](https://dimagi.sentry.io/issues/6362525737/?referrer=github-pr-bot)
- ‼️ **ESError: ConnectionTimeout caused by - ReadTimeoutError(HTTPConnectionPool(host='10.202.40.159', port=9200): Read timed out. (read timeout=30))** `/a/{domain}/reports/{var}|{var}|{var}|{var}|{va...` [View Issue](https://dimagi.sentry.io/issues/6362611659/?referrer=github-pr-bot)
- ‼️ **AttributeError: 'NoneType' object has no attribute 'timing_context'** `corehq.apps.saved_reports.tasks.send_email_report` [View Issue](https://dimagi.sentry.io/issues/6363054844/?referrer=github-pr-bot)

<sub>Did you find this useful? React with a 👍 or 👎</sub>
author_association: NONE
user: login: Charl1996
created_at: 2025-03-06T08:06:47Z
body: For the "`NoneType` object" bug, find [fix here](https://github.com/dimagi/commcare-hq/pull/35900).

At this stage I'm not sure that this PR is the cause of the rest of the noted issues, but looking into that as we speak. Maybe the `NoneType` bug caused it?
author_association: CONTRIBUTOR
user: login: Charl1996
created_at: 2025-03-06T09:07:31Z
body: > At this stage I'm not sure that this PR is the cause of the rest of the noted issues, but looking into that as we speak. Maybe the NoneType bug caused it?

The Case List Explorer on production can load, but with a very large data volume it fails to load. The Case List report is fine.

I don't see why it would be related to this PR, but still looking....
author_association: CONTRIBUTOR
user: login: mkangia
created_at: 2025-03-06T16:31:22Z
body: Hey @Charl1996 

I missed this earlier, can you add relevant labels on the PR please
author_association: CONTRIBUTOR
user: login: Charl1996
created_at: 2025-03-11T11:29:24Z
body: 
> I missed this earlier, can you add relevant labels on the PR please

Thanks, @mkangia.
author_association: CONTRIBUTOR