Skip to content

Commit 73e6cf0

Browse files
Merge pull request #9615 from cvat-ai/release-2.40.1
Release v2.40.1
2 parents 44e0618 + e480e9c commit 73e6cf0

File tree

13 files changed

+466
-67
lines changed

13 files changed

+466
-67
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
<!-- scriv-insert-here -->
1818

19+
<a id='changelog-2.40.1'></a>
20+
## \[2.40.1\] - 2025-07-07
21+
22+
### Fixed
23+
24+
- Low performance of DELELE `/api/tasks/<id>` and GET `/api/jobs(tasks)/<id>/annotations`
25+
Because of inefficient database queries (<https://github.com/cvat-ai/cvat/pull/9612>)
26+
1927
<a id='changelog-2.40.0'></a>
2028
## \[2.40.0\] - 2025-06-25
2129

cvat-cli/requirements/base.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cvat-sdk==2.40.0
1+
cvat-sdk==2.40.1
22

33
attrs>=24.2.0
44
Pillow>=10.3.0

cvat-cli/src/cvat_cli/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = "2.40.0"
1+
VERSION = "2.40.1"

cvat-sdk/gen/generate.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ set -e
88

99
GENERATOR_VERSION="v6.0.1"
1010

11-
VERSION="2.40.0"
11+
VERSION="2.40.1"
1212
LIB_NAME="cvat_sdk"
1313
LAYER1_LIB_NAME="${LIB_NAME}/api_client"
1414
DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)"

cvat-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cvat-ui",
3-
"version": "2.40.0",
3+
"version": "2.40.1",
44
"description": "CVAT single-page application",
55
"main": "src/index.tsx",
66
"scripts": {

cvat/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44

55
from cvat.utils.version import get_version
66

7-
VERSION = (2, 40, 0, "final", 0)
7+
VERSION = (2, 40, 1, "final", 0)
88

99
__version__ = get_version(VERSION)

cvat/apps/dataset_manager/task.py

Lines changed: 56 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,9 @@ def create_tracks(tracks, parent_track=None):
268268
self._validate_label_for_existence(db_track.label_id)
269269

270270
for attr in track_attributes:
271-
db_attr_val = models.LabeledTrackAttributeVal(**attr, track_id=len(db_tracks))
271+
db_attr_val = models.LabeledTrackAttributeVal(
272+
**attr, job_id=self.db_job.id, track_id=len(db_tracks)
273+
)
272274

273275
self._validate_attribute_for_existence(
274276
db_attr_val, db_track.label_id, "immutable"
@@ -282,7 +284,9 @@ def create_tracks(tracks, parent_track=None):
282284

283285
for attr in shape_attributes:
284286
db_attr_val = models.TrackedShapeAttributeVal(
285-
**attr, shape_id=len(db_shapes)
287+
**attr,
288+
shape_id=len(db_shapes),
289+
job_id=self.db_job.id,
286290
)
287291

288292
self._validate_attribute_for_existence(
@@ -345,7 +349,9 @@ def create_shapes(shapes, parent_shape=None):
345349
self._validate_label_for_existence(db_shape.label_id)
346350

347351
for attr in attributes:
348-
db_attr_val = models.LabeledShapeAttributeVal(**attr, shape_id=len(db_shapes))
352+
db_attr_val = models.LabeledShapeAttributeVal(
353+
**attr, job_id=self.db_job.id, shape_id=len(db_shapes)
354+
)
349355

350356
self._validate_attribute_for_existence(db_attr_val, db_shape.label_id, "all")
351357

@@ -382,7 +388,7 @@ def _save_tags_to_db(self, tags):
382388
self._validate_label_for_existence(db_tag.label_id)
383389

384390
for attr in attributes:
385-
db_attr_val = models.LabeledImageAttributeVal(**attr)
391+
db_attr_val = models.LabeledImageAttributeVal(**attr, job_id=self.db_job.id)
386392

387393
self._validate_attribute_for_existence(db_attr_val, db_tag.label_id, "all")
388394

@@ -574,35 +580,39 @@ def _extend_attributes(attributeval_set, default_attribute_values):
574580
)
575581

576582
def _init_tags_from_db(self):
577-
# NOTE: do not use .prefetch_related() with .values() since it's useless:
578-
# https://github.com/cvat-ai/cvat/pull/7748#issuecomment-2063695007
579-
db_tags = (
580-
self.db_job.labeledimage_set.values(
583+
db_tags = {
584+
row["id"]: dotdict(row, attributes=[])
585+
for row in self.db_job.labeledimage_set.values(
581586
"id",
582587
"frame",
583588
"label_id",
584589
"group",
585590
"source",
586-
"attribute__spec_id",
587-
"attribute__value",
588-
"attribute__id",
589591
)
590592
.order_by("frame")
591593
.iterator(chunk_size=2000)
592-
)
594+
}
593595

594-
db_tags = merge_table_rows(
595-
rows=db_tags,
596-
keys_for_merge={
597-
"attributes": [
598-
"attribute__spec_id",
599-
"attribute__value",
600-
"attribute__id",
601-
],
602-
},
603-
field_id="id",
604-
)
596+
for attr in self.db_job.labeledimageattributeval_set.values(
597+
"image_id",
598+
"spec_id",
599+
"value",
600+
"id",
601+
).iterator(chunk_size=2000):
602+
if attr["image_id"] not in db_tags:
603+
continue
604+
605+
db_tags[attr["image_id"]]["attributes"].append(
606+
dotdict(
607+
{
608+
"id": attr["id"],
609+
"spec_id": attr["spec_id"],
610+
"value": attr["value"],
611+
}
612+
)
613+
)
605614

615+
db_tags = list(db_tags.values())
606616
for db_tag in db_tags:
607617
self._extend_attributes(
608618
db_tag.attributes, self.db_attributes[db_tag.label_id]["all"].values()
@@ -612,10 +622,9 @@ def _init_tags_from_db(self):
612622
self.ir_data.tags = serializer.data
613623

614624
def _init_shapes_from_db(self):
615-
# NOTE: do not use .prefetch_related() with .values() since it's useless:
616-
# https://github.com/cvat-ai/cvat/pull/7748#issuecomment-2063695007
617-
db_shapes = (
618-
self.db_job.labeledshape_set.values(
625+
db_shapes = {
626+
row["id"]: dotdict(row, attributes=[])
627+
for row in self.db_job.labeledshape_set.values(
619628
"id",
620629
"label_id",
621630
"type",
@@ -628,29 +637,33 @@ def _init_shapes_from_db(self):
628637
"rotation",
629638
"points",
630639
"parent",
631-
"attribute__spec_id",
632-
"attribute__value",
633-
"attribute__id",
634640
)
635641
.order_by("frame")
636642
.iterator(chunk_size=2000)
637-
)
643+
}
638644

639-
db_shapes = merge_table_rows(
640-
rows=db_shapes,
641-
keys_for_merge={
642-
"attributes": [
643-
"attribute__spec_id",
644-
"attribute__value",
645-
"attribute__id",
646-
],
647-
},
648-
field_id="id",
649-
)
645+
for attr in self.db_job.labeledshapeattributeval_set.values(
646+
"shape_id",
647+
"spec_id",
648+
"value",
649+
"id",
650+
).iterator(chunk_size=2000):
651+
if attr["shape_id"] not in db_shapes:
652+
continue
653+
654+
db_shapes[attr["shape_id"]]["attributes"].append(
655+
dotdict(
656+
{
657+
"id": attr["id"],
658+
"spec_id": attr["spec_id"],
659+
"value": attr["value"],
660+
}
661+
)
662+
)
650663

651664
shapes = {}
652665
elements = {}
653-
for db_shape in db_shapes:
666+
for db_shape in db_shapes.values():
654667
self._extend_attributes(
655668
db_shape.attributes, self.db_attributes[db_shape.label_id]["all"].values()
656669
)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Generated by Django 4.2.21 on 2025-07-07 09:30
2+
3+
import django.db.models.deletion
4+
from django.db import connection, migrations, models
5+
from django.db.models import F, OuterRef, Subquery
6+
7+
8+
def set_attributeval_job_id(apps, schema_editor):
9+
LabeledImage = apps.get_model("engine", "LabeledImage")
10+
LabeledShape = apps.get_model("engine", "LabeledShape")
11+
LabeledTrack = apps.get_model("engine", "LabeledTrack")
12+
TrackedShape = apps.get_model("engine", "TrackedShape")
13+
14+
LabeledImageAttributeVal = apps.get_model("engine", "LabeledImageAttributeVal")
15+
LabeledShapeAttributeVal = apps.get_model("engine", "LabeledShapeAttributeVal")
16+
LabeledTrackAttributeVal = apps.get_model("engine", "LabeledTrackAttributeVal")
17+
TrackedShapeAttributeVal = apps.get_model("engine", "TrackedShapeAttributeVal")
18+
19+
LabeledImageAttributeVal.objects.annotate(
20+
related_job_id=Subquery(
21+
LabeledImage.objects.filter(id=OuterRef("image_id")).values("job_id")[:1]
22+
)
23+
).update(job_id=F("related_job_id"))
24+
25+
LabeledShapeAttributeVal.objects.annotate(
26+
related_job_id=Subquery(
27+
LabeledShape.objects.filter(id=OuterRef("shape_id")).values("job_id")[:1]
28+
)
29+
).update(job_id=F("related_job_id"))
30+
31+
LabeledTrackAttributeVal.objects.annotate(
32+
related_job_id=Subquery(
33+
LabeledTrack.objects.filter(id=OuterRef("track_id")).values("job_id")[:1]
34+
)
35+
).update(job_id=F("related_job_id"))
36+
37+
TrackedShapeAttributeVal.objects.annotate(
38+
related_job_id=Subquery(
39+
TrackedShape.objects.filter(id=OuterRef("shape_id")).values("track__job_id")[:1]
40+
)
41+
).update(job_id=F("related_job_id"))
42+
43+
44+
class Migration(migrations.Migration):
45+
46+
dependencies = [
47+
("engine", "0091_profile_last_activity_date"),
48+
]
49+
50+
operations = [
51+
migrations.AddField(
52+
model_name="labeledimageattributeval",
53+
name="job",
54+
field=models.ForeignKey(
55+
null=True,
56+
on_delete=django.db.models.deletion.DO_NOTHING,
57+
to="engine.job",
58+
),
59+
),
60+
migrations.AddField(
61+
model_name="labeledshapeattributeval",
62+
name="job",
63+
field=models.ForeignKey(
64+
null=True,
65+
on_delete=django.db.models.deletion.DO_NOTHING,
66+
to="engine.job",
67+
),
68+
),
69+
migrations.AddField(
70+
model_name="labeledtrackattributeval",
71+
name="job",
72+
field=models.ForeignKey(
73+
null=True,
74+
on_delete=django.db.models.deletion.DO_NOTHING,
75+
to="engine.job",
76+
),
77+
),
78+
migrations.AddField(
79+
model_name="trackedshapeattributeval",
80+
name="job",
81+
field=models.ForeignKey(
82+
null=True,
83+
on_delete=django.db.models.deletion.DO_NOTHING,
84+
to="engine.job",
85+
),
86+
),
87+
migrations.RunPython(set_attributeval_job_id, reverse_code=migrations.RunPython.noop),
88+
migrations.AlterField(
89+
model_name="labeledimageattributeval",
90+
name="job",
91+
field=models.ForeignKey(
92+
null=False,
93+
on_delete=django.db.models.deletion.DO_NOTHING,
94+
to="engine.job",
95+
),
96+
),
97+
migrations.AlterField(
98+
model_name="labeledshapeattributeval",
99+
name="job",
100+
field=models.ForeignKey(
101+
null=False,
102+
on_delete=django.db.models.deletion.DO_NOTHING,
103+
to="engine.job",
104+
),
105+
),
106+
migrations.AlterField(
107+
model_name="labeledtrackattributeval",
108+
name="job",
109+
field=models.ForeignKey(
110+
null=False,
111+
on_delete=django.db.models.deletion.DO_NOTHING,
112+
to="engine.job",
113+
),
114+
),
115+
migrations.AlterField(
116+
model_name="trackedshapeattributeval",
117+
name="job",
118+
field=models.ForeignKey(
119+
null=False,
120+
on_delete=django.db.models.deletion.DO_NOTHING,
121+
to="engine.job",
122+
),
123+
),
124+
]

cvat/apps/engine/models.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -459,13 +459,13 @@ def get_tmp_dirname(self) -> str:
459459
@transaction.atomic(savepoint=False)
460460
def clear_annotations_in_jobs(job_ids: Iterable[int]):
461461
for job_ids_chunk in take_by(job_ids, chunk_size=1000):
462-
TrackedShapeAttributeVal.objects.filter(shape__track__job_id__in=job_ids_chunk).delete()
462+
TrackedShapeAttributeVal.objects.filter(job_id__in=job_ids_chunk).delete()
463463
TrackedShape.objects.filter(track__job_id__in=job_ids_chunk).delete()
464-
LabeledTrackAttributeVal.objects.filter(track__job_id__in=job_ids_chunk).delete()
464+
LabeledTrackAttributeVal.objects.filter(job_id__in=job_ids_chunk).delete()
465465
LabeledTrack.objects.filter(job_id__in=job_ids_chunk).delete()
466-
LabeledShapeAttributeVal.objects.filter(shape__job_id__in=job_ids_chunk).delete()
466+
LabeledShapeAttributeVal.objects.filter(job_id__in=job_ids_chunk).delete()
467467
LabeledShape.objects.filter(job_id__in=job_ids_chunk).delete()
468-
LabeledImageAttributeVal.objects.filter(image__job_id__in=job_ids_chunk).delete()
468+
LabeledImageAttributeVal.objects.filter(job_id__in=job_ids_chunk).delete()
469469
LabeledImage.objects.filter(job_id__in=job_ids_chunk).delete()
470470

471471

@@ -477,15 +477,15 @@ def clear_annotations_on_frames_in_honeypot_task(db_task: Task, frames: Sequence
477477

478478
for frames_batch in take_by(frames, chunk_size=1000):
479479
LabeledShapeAttributeVal.objects.filter(
480-
shape__job_id__segment__task_id=db_task.id,
480+
job_id__segment__task_id=db_task.id,
481481
shape__frame__in=frames_batch,
482482
).delete()
483483
LabeledShape.objects.filter(
484484
job_id__segment__task_id=db_task.id,
485485
frame__in=frames_batch,
486486
).delete()
487487
LabeledImageAttributeVal.objects.filter(
488-
image__job_id__segment__task_id=db_task.id,
488+
job_id__segment__task_id=db_task.id,
489489
image__frame__in=frames_batch,
490490
).delete()
491491
LabeledImage.objects.filter(
@@ -1075,6 +1075,7 @@ class AttributeVal(models.Model):
10751075
# TODO: add a validator here to be sure that it corresponds to self.label
10761076
id = models.BigAutoField(primary_key=True)
10771077
spec = models.ForeignKey(AttributeSpec, on_delete=models.CASCADE)
1078+
job = models.ForeignKey(Job, on_delete=models.DO_NOTHING, null=False)
10781079
value = SafeCharField(max_length=4096)
10791080

10801081
class Meta:

cvat/schema.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
openapi: 3.0.3
22
info:
33
title: CVAT REST API
4-
version: 2.40.0
4+
version: 2.40.1
55
description: REST API for Computer Vision Annotation Tool (CVAT)
66
termsOfService: https://www.google.com/policies/terms/
77
contact:

0 commit comments

Comments
 (0)