Skip to content

perf(tree-details): filter and cache builds, boots, and tests in SQL - #2206

Merged
felipebergamin merged 4 commits into
kernelci:mainfrom
profusion:perf/2202/tree-details-memory
Oct 29, 2026
Merged

felipebergamin merged 4 commits into
kernelci:mainfrom
profusion:perf/2202/tree-details-memory

Conversation

@felipebergamin

Copy link
Copy Markdown
Member

Summary

Closes #2202 by moving tree detail filtering into SQL for builds, boots, and tests. Each query selects only fields used by the response and returns at most one matching incident per item. The endpoint caches the final response instead of raw database rows.

The existing response shape and complete-result behavior remain unchanged. This does not add pagination or a result limit. It also keeps the worker recycling and container memory limits introduced by #2203.

Performance measurements

Using the same production commit and five requests per tab in separate processes:

Tab Original peak RSS Optimized peak RSS SQL rows returned
Builds 91.74 MiB 89.15 MiB 298 → 234
Boots 91.74 MiB 89.15 MiB 298 → 234
Tests 460.30 MiB 209.75 MiB 44,235 → 44,224

RSS measured the legacy and optimized BaseView request paths with JSON rendering and explicit garbage collection. This is a short, single-process sample, not a concurrency or long-duration leak test. Response bodies were equivalent between implementations.

Validation

  • 1,339 SQL differential integration tests passed against disposable PostgreSQL.
  • 978 backend unit tests passed.
  • Production read-only comparisons passed for builds, boots, and tests.
  • Runtime HTTP checks returned equivalent cold and warm responses for all three tabs.
  • Generated API schema matches backend/schema.yml.

To reproduce the SQL tests, start the local integration test services and database as described in IntegrationTests.md, then run from backend/:

DB_HOST=127.0.0.1 DB_PORT=5435 DJANGO_SETTINGS_MODULE=kernelCI.test_settings poetry run pytest -m integration --use-local-db --run-all \
  kernelCI_app/tests/integrationTests/treeDetailsBuildsSql_test.py \
  kernelCI_app/tests/integrationTests/treeDetailsTestHistorySql_test.py

Related: #2202, #2203

Restore Builds with one row per build and cache validated responses. Preserve existing filter and empty-result behavior.

Part of kernelci#2202

Assisted-by: OpenAI Codex
Signed-off-by: Felipe Bergamin <felipebergamin@profusion.mobi>
Filter before transfer, select one incident per boot and cache the final response. Preserve hardware fallback and full_environment_misc behavior.

Part of kernelci#2202

Assisted-by: OpenAI Codex
Signed-off-by: Felipe Bergamin <felipebergamin@profusion.mobi>
Use the filtered history query for non-boot tests, keeping separate cache entries and existing filter semantics.

Part of kernelci#2202

Assisted-by: OpenAI Codex
Signed-off-by: Felipe Bergamin <felipebergamin@profusion.mobi>
Restore CLI integration assertions for the re-enabled builds endpoint.

Part of kernelci#2202
Assisted-by: Codex (GPT-6)

Signed-off-by: Felipe Bergamin <felipebergamin@profusion.mobi>
commit_hash: str,
tree_name: Optional[str],
) -> dict[str, Any]:
where, issue_where, params = _filter_clauses(filters)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be a nit, but we could just add the filters on params, this way filter_clauses keeps a single purpose

"""
builds = []
error_message = None
with connection.cursor() as cursor:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoiding the fetch all might be a good approach here. But we could get a smaller footprint if we stream the rows using cursor.stream.
Might be a good idea to take a look on the time spent here, as we might need to find a good compromise on batch size and connection time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small test here, using a 2000 batch size.

fetchall execute + for stream
Peak RSS 354 MiB 169 MiB 82 MiB
Time to finish 46.8 s 43.3 s 45.5 s
First byte 46.8 s 40.1 s 2.8 s
Connection held 46.4 s 43.3 s 45.5 s

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small test here, using a 2000 batch size.

fetchall execute + for stream
Peak RSS 354 MiB 169 MiB 82 MiB
Time to finish 46.8 s 43.3 s 45.5 s
First byte 46.8 s 40.1 s 2.8 s
Connection held 46.4 s 43.3 s 45.5 s

How did you do this tests?

I tested this through response construction, Redis caching, and JSON rendering. With 100k synthetic rows, stream(size=2000) reduced peak RSS from 651 to 565 MiB, with similar total time, 6.16 vs 6.11 seconds.

Something that might become a problem is that cursor.stream() bypasses Django’s execution hooks and exception translation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a small script for comparison to keep only the query for next/master. But it seems you got roughly 90MiB extra, as I got.
There could be better numbers than 2000 to be used (it was just a guess from my part).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if understood the problem of the django execution hook. But feel free to ignore if it could be problematic for validation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if understood the problem of the django execution hook. But feel free to ignore if it could be problematic for validation.

AFAIK Django normally wraps database execution to support query logging, instrumentation, and conversion of driver exceptions into Django exceptions. Calling cursor.stream() directly bypasses that execution path.

It’s manageable, not a reason to reject streaming. Just something to have in mind.

I did a small script for comparison to keep only the query for next/master. But it seems you got roughly 90MiB extra, as I got. There could be better numbers than 2000 to be used (it was just a guess from my part).

I ran with different batch sizes and got similar numbers. I think most of the remaining memory is somewhere else

) selected_issue ON TRUE
WHERE {where}
)
SELECT metadata.checkout_count, metadata.has_builds,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we are moving the filter to inside the query, we might be able to drop some of those columns.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we still should remember that moving filter inside, might make we need to do more long fetches when applying filters. If using cursor.stream help us enough with saving memory, might be worth keeping filters out.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might not be a big problem if we also consider #2207

@alanpeixinho alanpeixinho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some possible improvements as comments, but overall, it is a great improvement already

@felipebergamin
felipebergamin added this pull request to the merge queue Oct 29, 2026
Merged via the queue into kernelci:main with commit 9dbee1c Oct 29, 2026
7 checks passed
@felipebergamin
felipebergamin deleted the perf/2202/tree-details-memory branch October 29, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tree builds endpoint exhausts memory and took production down

2 participants