Skip to content

perf(values): depth check keeps one frame per level -- 80% less memory - #318

Open
dexhunter wants to merge 3 commits into
graphql-python:mainfrom
dexhunter:perf/nested-input-depth-check-memory
Open

dexhunter wants to merge 3 commits into
graphql-python:mainfrom
dexhunter:perf/nested-input-depth-check-memory

Conversation

@dexhunter

Copy link
Copy Markdown
Contributor

TL;DR

  • Lower the peak memory of input_is_nested_too_deeply, the depth check for variable values.
  • Keep one stack frame per nesting level and do not push one tuple per list item.
  • tracemalloc peak for a [Int] variable of 50,000 ints: 4,091,392 bytes -> 818,981 bytes, 80.0% less.
  • Native tests: PYTHONPATH=src python3 -m pytest tests/execution/test_variables.py -q passed 106 tests.

Refs #317

What & Why

Before, the check pushed one (item, type, depth) tuple per list item onto its stack before it popped any.
The memory for the check grew with the width of the list, about 70 bytes per item.

Now each stack frame holds one nesting level: the values, their type, the depth, and the next index.
The check also stops the walk of a level when the item type is not a list or an input object.
Such items cannot nest, so one depth check covers the full level.

The output stays identical because the check still visits each followed value at the same depth.
Empty lists still add no depth, and values under scalars are still not followed.

Validation

Peak traced memory (tracemalloc) and best time of one call to the check:

Situation Before After Change
[Int], 50,000 ints 3,516,880 bytes, 11.50 ms 112 bytes, 0.0005 ms memory no longer grows with the list
[Int], 10,000 ints 597,680 bytes, 2.16 ms 112 bytes, 0.0005 ms memory no longer grows with the list
[[Int]], 500 lists of 100 ints 5,936 bytes, 10.58 ms 176 bytes, 0.23 ms about 46x less time
[Point], 5,000 input objects 234,608 bytes, 8.59 ms 712 bytes, 7.25 ms 16% less time
Int, one value 128 bytes, 0.0003 ms 88 bytes, 0.0003 ms no measurable change in time

The last row shows a case where the change does not help, and it shows no slowdown.
The number in the TL;DR is for the full get_variable_values call, so it includes the coerced result.
Measured on CPython 3.12.13, Linux x86-64, AMD EPYC 9R14, on one isolated core.

Script for the first row; run it on the base commit and on this change:

import tracemalloc
from graphql import GraphQLInt, GraphQLList
from graphql.execution.values import input_is_nested_too_deeply

value = list(range(50_000))
tracemalloc.start()
input_is_nested_too_deeply(value, GraphQLList(GraphQLInt))
print(tracemalloc.get_traced_memory()[1])

The complete suite also passed with PYTHONPATH=src python3 -m pytest tests -q: 3,821 passed, 18 skipped.
ruff check, ruff format --check (ruff 0.17.4) and mypy (2.4.1) passed for src/graphql/execution/values.py.

Why this is safe and easy to maintain

The change is small and easy to read: 13 added lines in one private function, with no new imports.
The function keeps its loop, its explicit stack, and its signature.
Behaviour is unchanged, and the returned bool is the same for every value and type.
The tests in tests/execution/test_variables.py cover the depth limit for lists, input objects, and custom scalars.

Scope

This changes Python bookkeeping in a private helper only.
It adds no feature and does not change GraphQL behavior or the public API.

AI assistance

This PR was prepared with AI assistance from Aiden. Candidates were tracked with Weco Observe.

Weco share link: https://dashboard.weco.ai/share/yE6YtGpMB4NV3RCoYfCM3tHnAk6srhv_

input_is_nested_too_deeply pushed one tuple per list item onto its stack
before it popped any, so its memory grew with the width of the list.
Keep one frame per nesting level (values, type, depth, next index) and
stop the walk of a level when its item type cannot nest.

Coercing a [Int] variable of 50,000 ints has a tracemalloc peak of
818,981 bytes, down from 4,091,392 bytes. The returned bool is unchanged.

Co-Authored-By: Aiden <aiden@weco.ai>
@dexhunter
dexhunter requested a review from Cito as a code owner November 10, 2026 20:15
@codspeed

codspeed Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 20 untouched benchmarks


Comparing dexhunter:perf/nested-input-depth-check-memory (0259ce7) with main (1d505c1)

Open in CodSpeed

dexhunter and others added 2 commits November 10, 2026 20:27
The depth check keeps one frame per nesting level. No test used a list
with several nested items or an empty list, so one line and one branch
were not covered and the 100% coverage gate failed on Python 3.14.

Co-Authored-By: Aiden <aiden@weco.ai>
Reuse the deep list type of the existing test instead of building it
again in a second test.

Co-Authored-By: Aiden <aiden@weco.ai>
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.

1 participant