Skip to content

Commit d2ae4fa

Browse files
jedcunninghamRatasa143
authored andcommitted
Flatten grid structure endpoint memory consumption (apache#61273)
The grid structure endpoint was loading all serdags for the shown dagruns into memory at once, before merging them together. Now, we load 5 at a time and also expunge so they can be gc'd more quickly.
1 parent 76aa78c commit d2ae4fa

1 file changed

Lines changed: 31 additions & 19 deletions

File tree

  • airflow-core/src/airflow/api_fastapi/core_api/routes/ui

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ def get_dag_structure(
143143
"""Return dag structure for grid view."""
144144
latest_serdag = _get_latest_serdag(dag_id, session)
145145
latest_dag = latest_serdag.dag
146+
latest_serdag_id = latest_serdag.id
147+
session.expunge(latest_serdag) # allow GC of serdag; only latest_dag is needed from here
146148

147149
# Apply filtering if root task is specified
148150
if root:
@@ -176,12 +178,22 @@ def get_dag_structure(
176178
nodes = [task_group_to_dict_grid(x) for x in task_group_sort(latest_dag.task_group)]
177179
return [GridNodeResponse(**n) for n in nodes]
178180

179-
serdags = session.scalars(
180-
select(SerializedDagModel).where(
181+
# Process and merge the latest serdag first
182+
merged_nodes: list[dict[str, Any]] = []
183+
nodes = [task_group_to_dict_grid(x) for x in task_group_sort(latest_dag.task_group)]
184+
_merge_node_dicts(merged_nodes, nodes)
185+
del latest_dag
186+
187+
# Process serdags one by one and merge immediately to reduce memory usage.
188+
# Use yield_per() for streaming results and expunge each serdag after processing
189+
# to allow garbage collection and prevent memory buildup in the session identity map.
190+
serdags_query = (
191+
select(SerializedDagModel)
192+
.where(
181193
# Even though dag_id is filtered in base_query,
182194
# adding this line here can improve the performance of this endpoint
183195
SerializedDagModel.dag_id == dag_id,
184-
SerializedDagModel.id != latest_serdag.id,
196+
SerializedDagModel.id != latest_serdag_id,
185197
SerializedDagModel.dag_version_id.in_(
186198
select(TaskInstance.dag_version_id)
187199
.join(TaskInstance.dag_run)
@@ -191,25 +203,25 @@ def get_dag_structure(
191203
.distinct()
192204
),
193205
)
206+
.execution_options(yield_per=5) # balance between peak memory usage and round trips
194207
)
195-
merged_nodes: list[dict[str, Any]] = []
196-
dags = [latest_dag]
197-
for serdag in serdags:
198-
if serdag:
199-
filtered_dag = serdag.dag
200-
# Apply the same filtering to historical DAG versions
201-
if root:
202-
filtered_dag = filtered_dag.partial_subset(
203-
task_ids=root,
204-
include_upstream=include_upstream,
205-
include_downstream=include_downstream,
206-
depth=depth,
207-
)
208-
dags.append(filtered_dag)
209-
for dag in dags:
210-
nodes = [task_group_to_dict_grid(x) for x in task_group_sort(dag.task_group)]
208+
209+
for serdag in session.scalars(serdags_query):
210+
filtered_dag = serdag.dag
211+
# Apply the same filtering to historical DAG versions
212+
if root:
213+
filtered_dag = filtered_dag.partial_subset(
214+
task_ids=root,
215+
include_upstream=include_upstream,
216+
include_downstream=include_downstream,
217+
depth=depth,
218+
)
219+
# Merge immediately instead of collecting all DAGs in memory
220+
nodes = [task_group_to_dict_grid(x) for x in task_group_sort(filtered_dag.task_group)]
211221
_merge_node_dicts(merged_nodes, nodes)
212222

223+
session.expunge(serdag) # to allow garbage collection
224+
213225
return [GridNodeResponse(**n) for n in merged_nodes]
214226

215227

0 commit comments

Comments
 (0)