0

I have problem with my endpoint performance which returns around 40 items and response takes around 17 seconds.

I have model:

class GameTask(models.Model):
    name= models.CharField()
    description = RichTextUploadingField()
    ...

and another model like that:

class TaskLevel(models.Model):
    master_task = models.ForeignKey(GameTask, related_name="sub_levels", on_delete-models.CASCADE)
    sub_tasks = models.ManyToManyField(GameTask, related_name="master_levels")
    ...

So basicly I can have "normal" tasks, but when I create TaskLevel object I can add master_task as a task which gonna fasten other tasks added to sub_tasks field.

My serializers look like:

class TaskBaseSerializer(serializers.ModelSerializer):
    fields created by serializers.SerializerMethodField()
    ...

class TaskLevelSerializer(serializers.ModelSerializer):
    sub_tasks = serializers.SerializerMethodField()

    class Meta:
        model = TaskLevel

    def get_sub_tasks(self, obj: TaskLevel):
        sub_tasks = get_sub_tasks(level=obj, user=self.context["request"].user) # method from other module
        return TaskBaseSerializer(sub_tasks, many=True, context=self.context).data

class TaskSerializer(TaskBaseSerializer):
    levels_config = serializers.SerializerMethodField()

    class Meta:
        model = GameTask

    def get_levels_config(self, obj: GameTask):
        if is_mastertask(obj):
            return dict(
                sub_levels=TaskLevelSerializer(
                    obj.sub_levels.all().order_by("number"), many=True, context=self.context
                ).data,
                progress=get_progress(
                    master_task=obj, user=self.context["request"].user
                ),
            )
        return None

When I tried to measure time it turned out that get_levels_config method takes around 0.25 seconds for one multilevel-task (which contain 7 subtasks). Is there any way to improve this performance? If any more detailed methods are needed I will add them

Frendom
  • 508
  • 6
  • 24

1 Answers1

2

Your code might be suffering from N+1 problem. TaskSerializer.get_levels_config() performs database queries from obj.sub_levels.all().order_by("number").

What happens when serializing multiple instances like:

TaskSerializer(tasks, many=True)

each instance calls .get_levels_config()

You can use prefetch_related & selected_related(more explanation here).

You will have to manually check for prefetched objects since you are using SerializerMethodField. There's also the functions get_progress & get_sub_tasks which I assume does another query.


Here are some examples that can be used around your code:

Prefetching:

GameTask.objects.prefetch_related("sub_levels", "master_levels")

# Accessing deeper level
GameTask.objects.prefetch_related(
    "sub_levels",
    "master_levels",
    "sub_levels__sub_tasks",
).select_related(
    "master_levels__master_task",
)

Checking prefetch:

def get_sub_tasks(self, obj: TaskLevel):
    if hasattr(obj, "_prefetched_objects_cache") and obj._prefetched_objects_cache.get("sub_tasks", None):
        return obj._prefetched_objects_cache
    return obj.sub_tasks.all()
Kyell
  • 621
  • 5
  • 9