Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 54 additions & 10 deletions airflow/models/taskinstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -2415,34 +2415,78 @@ def filter_for_tis(tis: Iterable[TaskInstance | TaskInstanceKey]) -> BooleanClau
run_id = first.run_id
map_index = first.map_index
first_task_id = first.task_id

# pre-compute the set of dag_id, run_id, map_indices and task_ids
dag_ids, run_ids, map_indices, task_ids = set(), set(), set(), set()
for t in tis:
dag_ids.add(t.dag_id)
run_ids.add(t.run_id)
map_indices.add(t.map_index)
task_ids.add(t.task_id)

# Common path optimisations: when all TIs are for the same dag_id and run_id, or same dag_id
# and task_id -- this can be over 150x faster for huge numbers of TIs (20k+)
if all(t.dag_id == dag_id and t.run_id == run_id and t.map_index == map_index for t in tis):
if dag_ids == {dag_id} and run_ids == {run_id} and map_indices == {map_index}:
return and_(
TaskInstance.dag_id == dag_id,
TaskInstance.run_id == run_id,
TaskInstance.map_index == map_index,
TaskInstance.task_id.in_(t.task_id for t in tis),
TaskInstance.task_id.in_(task_ids),
)
if all(t.dag_id == dag_id and t.task_id == first_task_id and t.map_index == map_index for t in tis):
if dag_ids == {dag_id} and task_ids == {first_task_id} and map_indices == {map_index}:
return and_(
TaskInstance.dag_id == dag_id,
TaskInstance.run_id.in_(t.run_id for t in tis),
TaskInstance.run_id.in_(run_ids),
TaskInstance.map_index == map_index,
TaskInstance.task_id == first_task_id,
)
if all(t.dag_id == dag_id and t.run_id == run_id and t.task_id == first_task_id for t in tis):
if dag_ids == {dag_id} and run_ids == {run_id} and task_ids == {first_task_id}:
return and_(
TaskInstance.dag_id == dag_id,
TaskInstance.run_id == run_id,
TaskInstance.map_index.in_(t.map_index for t in tis),
TaskInstance.map_index.in_(map_indices),
TaskInstance.task_id == first_task_id,
)
Comment on lines 2420 to 2449

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel these three fast paths shouldn’t be changed, the new code does not look faster to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this is a very small optimisation in the grand scheme of things, but it does improve the speed by a little (the vast majority of the optimisation is below, in the improved sql query).

instead of constantly iterating through the tis to obtain the various attributes to compare with a static value, in 3 different conditional statements, we instead pre-compute those attributes into a set and compare that with a static set

some small benchmarks on this:

In [1]: x = [5 for i in range(1000000)]

In [2]: %timeit all(i == 5 for i in x)
43.6 ms ± 2.01 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [3]: %timeit set(x) == {5}
8.02 ms ± 179 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

clearing the tasks which fulfils the 3rd condition in these 3 fast paths gives me:

old

[2022-11-02 13:48:27,970] {views.py:2017} INFO - Number of tasks to clear: 500
[2022-11-02 13:48:27,970] {views.py:2018} INFO - Time taken: 0.08926955598872155

new

[2022-11-02 13:52:04,373] {views.py:2017} INFO - Number of tasks to clear: 500
[2022-11-02 13:52:04,373] {views.py:2018} INFO - Time taken: 0.060697362991049886


return tuple_in_condition(
(TaskInstance.dag_id, TaskInstance.task_id, TaskInstance.run_id, TaskInstance.map_index),
(ti.key.primary for ti in tis),
)
filter_condition = []
# create 2 nested groups, both primarily grouped by dag_id and run_id,
# and in the nested group 1 grouped by task_id the other by map_index.
task_id_groups: dict[tuple, dict[Any, list[Any]]] = defaultdict(lambda: defaultdict(list))
map_index_groups: dict[tuple, dict[Any, list[Any]]] = defaultdict(lambda: defaultdict(list))
for t in tis:
task_id_groups[(t.dag_id, t.run_id)][t.task_id].append(t.map_index)
map_index_groups[(t.dag_id, t.run_id)][t.map_index].append(t.task_id)

# this assumes that most dags have dag_id as the largest grouping, followed by run_id. even
# if its not, this is still a significant optimization over querying for every single tuple key
for cur_dag_id in dag_ids:
for cur_run_id in run_ids:
# we compare the group size between task_id and map_index and use the smaller group
dag_task_id_groups = task_id_groups[(cur_dag_id, cur_run_id)]
dag_map_index_groups = map_index_groups[(cur_dag_id, cur_run_id)]

if len(dag_task_id_groups) <= len(dag_map_index_groups):
for cur_task_id, cur_map_indices in dag_task_id_groups.items():
filter_condition.append(
and_(
TaskInstance.dag_id == cur_dag_id,
TaskInstance.run_id == cur_run_id,
TaskInstance.task_id == cur_task_id,
TaskInstance.map_index.in_(cur_map_indices),
)
)
else:
for cur_map_index, cur_task_ids in dag_map_index_groups.items():
filter_condition.append(
and_(
TaskInstance.dag_id == cur_dag_id,
TaskInstance.run_id == cur_run_id,
TaskInstance.task_id.in_(cur_task_ids),
TaskInstance.map_index == cur_map_index,
)
)

return or_(*filter_condition)

@classmethod
def ti_selector_condition(cls, vals: Collection[str | tuple[str, int]]) -> ColumnOperators:
Expand Down