Page loading is very slow - how to properly optimize?

70 views Asked by At

I'm working on a big page with different product statistics. I use there multiple charts, tables etc. It takes for example 5 seconds to load the page.

This is a very simplified models.py

EDIT

For example, OccurencesTable contains this row:

last_scan_time = tables.columns.TemplateColumn("""{{ record.get_last_scan.datetime }}""",accessor='get_last_scan.datetime', verbose_name='Last scan time')

So for each row, there must be executed query to the database which calls last scan. Better approach would be to preload all Scan objects.

PRODUCT

class Product(models.Model):
    user = models.ForeignKey(User, null=False, blank=False, related_name='products')
    name = models.CharField(max_length=200)

    def get_occurences(self):
        return self.occurences.prefetch_related("scans__price__currency")

    def get_occurences_count(self):
        return self.occurences.all().count()

    def get_all_scans(self):
        return [item for sublist in [x.scans.all() for x in self.get_occurences()] for item in sublist]

    def get_all_valid_scans(self):
        return [item for sublist in [x.get_valid_scans() for x in self.get_occurences()] for item in sublist]

    def get_last_scans(self):
        scans = []
        for occ in self.get_occurences():
            scan = occ.get_last_scan()
            if scan:
                scans.append(scan)
        return scans

   # @property
    #def last_scan_time(self):
        #scans = sorted([x for x in self.get_last_scans() if x.datetime],key = lambda x: x.datetime, reverse=True)
        #Scan.objects.filter(occurence__product=self,price__amount__isnull=False)
        #return str(scans[0].datetime)

    def get_last_min_scan(self):

        sorted_last_scans = [x for x in self.get_last_scans() if x.valid]
        sorted_last_scans.sort(key=lambda x: x.price.eur_price)
        return sorted_last_scans[0] if sorted_last_scans else None

    def get_last_min_price(self):
        last_scan = self.get_last_min_scan()
        if last_scan:
            return last_scan.price.eur_price
        return None

    def get_active_occurences(self):
        return self.get_occurences().filter(active=True)

OCCURENCE

class Occurence(models.Model):
    product = models.ForeignKey(Product, related_name='occurences', on_delete=models.CASCADE)

    _last_scan = models.OneToOneField('Scan',null=True,blank=True,related_name='+')


    currency = models.ForeignKey('Currency',related_name='occurences')

    def get_last_scan(self):
        try:
            last = self.scans.select_related("price__amount").order_by('datetime').last()
        except:
            last = None
        return last

    def get_last_valid_scan(self):
        try:
            last = self.scans.exclude(price__isnull=True).order_by('-datetime').first()
        except:
            last = None
        return last

    def get_second_last_valid_scan(self):
        scans = self.scans.exclude(price__isnull=True).order_by('-datetime').select_related("price")
        if scans.count()>=2:
            return scans[1]
        return None

    def get_valid_scans(self):
        return self.scans.all().exclude(price__isnull=True)

    def get_min_scan(self):
        scan = self.get_valid_scans().order_by('price__amount').first()
        if scan:
            return scan
        return None

    """ STATS METHODS """

    def stats_get_difference_for_two_last_scans(self):
        second_last_valid_scan = self.get_second_last_valid_scan()
        if second_last_valid_scan:
            difference_in_percent = math_ops.round_eur(decimal.Decimal(-1 * (100 - self.get_last_valid_scan().price.eur_price / second_last_valid_scan.price.eur_price * 100), 2))
        else:
            difference_in_percent = decimal.Decimal(0)
        return {'percent':difference_in_percent,
                'xml_tag':'<two_last_scans_difference_in_percent>',
                'xml_close_tag':'</two_last_scans_difference_in_percent>',
                'label':'Last scans diff'}

    def stats_get_min_price(self):
        scan = self.get_min_scan()
        if scan:
            price = scan.price.eur_price
        else:
            price = None
        return {'price': price,
                'xml_tag': '<min_price>',
                'xml_close_tag': '</min_price>',
                'label': 'Min'}

    def stats_get_avg_price(self):
        prices = [x.price for x in self.scans.all() if x.price]
        if prices:
            price = math_ops.round_eur(decimal.Decimal(sum([x.eur_price for x in prices]) / len(prices), 2))
        else:
            price = None

        preferred_currency = self.product.user.userprofile.preferred_currency

        if preferred_currency:
            if preferred_currency.shortcut == 'czk':
                amount = Exchange.eur_to_czk(price)
                pref_currency_string = '{} CZK'.format(amount)
                pref_currency_amount = amount
            else:
                amount = price
                pref_currency_string = u'{} €'.format(amount)
                pref_currency_amount = amount
        else:
            if self.currency.shortcut == 'czk':
                amount = Exchange.eur_to_czk(price)
                pref_currency_string = '{} CZK'.format(amount)
                pref_currency_amount = amount
            else:
                amount = price
                pref_currency_string = u'{} €'.format(amount)
                pref_currency_amount = amount


        return {'price': price,
                'pref_currency_string':pref_currency_string,
                'pref_currency_amount':pref_currency_amount,
                'xml_tag': '<average_price>',
                'xml_close_tag': '</average_price>',
                'label': 'AVG'}

PRICE

class Price(models.Model):
    currency = models.ForeignKey('Currency',related_name='prices')
    amount = models.DecimalField(max_digits=10,decimal_places=2)


    def __unicode__(self):
        return u'{} {}'.format(self.amount,self.currency)

    def to_eur(self):
        if self.currency.shortcut=='eur':
            return self.amount
        elif self.currency.shortcut=='czk':
            return Exchange.objects.first().czk_to_eur(self.amount)

    def to_czk(self):
        if self.currency.shortcut == 'czk':
            return self.amount
        elif self.currency.shortcut == 'eur':
            return Exchange.objects.first().eur_to_czk(self.amount)

    @property
    def eur_price(self):
        if self.currency.shortcut=='eur':
            return self.amount
        elif self.currency.shortcut=='czk':
            return self.to_eur()

    @property
    def czk_price(self):
        cents = decimal.Decimal('01')
        if self.currency.shortcut == 'czk':
            return (self.amount).quantize(cents, decimal.ROUND_HALF_UP)
        elif self.currency.shortcut == 'eur':
            return self.to_czk()

    @property
    def pref_currency_amount(self):
        pref_currency = self.scan.occurence.product.user.userprofile.preferred_currency
        if pref_currency:
            if pref_currency.shortcut == 'czk':
                return self.czk_price
            else: return self.eur_price
        return self.amount

    @property
    def pref_currency_string(self):
        pref_currency = self.scan.occurence.product.user.userprofile.preferred_currency
        # return pref_currency.shortcut
        if pref_currency:
            if pref_currency.shortcut.lower() == 'czk':
                return u'{} {}'.format(self.czk_price, pref_currency.shortcut)
            else:
                return u'{} {}'.format(self.eur_price, pref_currency.special_sign)
        return u'{} {}'.format(self.amount,self.currency.special_sign)


    def get_price(self,currency):
        if currency=='eur':
            return self.eur_price
        elif currency=='czk':
            return self.czk_price

    def get_exchanged_price_string(self):
        if self.currency.shortcut=='czk':
            return u'{} {}'.format(Exchange.czk_to_eur(self.amount),u'€')
        else:
            return u'{} {}'.format(Exchange.eur_to_czk(self.amount),'CZK')

    def get_original_price_string(self):
        if self.currency.shortcut=='czk':
            return u'{} {}'.format(self.amount,u'€')
        else:
            return u'{} {}'.format(Exchange.eur_to_czk(self.amount),'CZK')

For example rendering table occurences takes almost 2 seconds according to django-debug-toolbar. I'm trying to optimize it using select_related and prefetch_related but its still slow.

It's because of different methods I call has the same queries and those queries are called multiple times.

class OccurencesTable(tables.Table):
    site = tables.columns.TemplateColumn("""<a href="{{ record.url }}">{{ record.site.name }}</a>""",accessor='site.name', verbose_name=u'Site')
    avg_price = tables.columns.TemplateColumn("""{{ record.stats_get_avg_price.pref_currency_string }}""",accessor='stats_get_avg_price.price', verbose_name='AVG price')
    last_scan_price = tables.columns.TemplateColumn("""{{ record.get_last_scan.price.pref_currency_string }} """,accessor='get_last_scan.price.amount', verbose_name='Last scan price')
    last_scan_time = tables.columns.TemplateColumn("""{{ record.get_last_scan.datetime }}""",accessor='get_last_scan.datetime', verbose_name='Last scan time')
    difference = tables.columns.TemplateColumn("""{% load static %}{% with diff=record.stats_get_difference_for_two_last_scans.percent %}
                {% if diff > 0 %}+{% endif %}{{ diff }} % <img style="height: 15px" src="{% if diff < 0 %}{% static "img/icons/arrow-trend-minus.png" %}{% elif diff == 0 %}{% static "img/icons/arrow-trend-normal.png" %}{% else %}{% static "img/icons/arrow-trend-plus.png" %}{% endif %}">
                {% endwith %}""",verbose_name='Difference')

    class Meta:
        model = Occurence
        fields = ('id', 'site', 'last_scan_time','last_scan_price', 'difference',  'avg_price')
        attrs = {'id': 'id_occurences_table',
                 'class': 'table', }

Can't figure out how to optimize methods of models Occurence and Product. Do you have any ideas?

1

There are 1 answers

3
e4c5 On

With code like this, you should be thrilled with the timing that you get

@property
def last_scan_time(self):
    scans = sorted([x for x in self.get_last_scans() if x.datetime],key = lambda x: x.datetime, reverse=True)
    Scan.objects.filter(occurence__product=self,price__amount__isnull=False)
    return str(scans[0].datetime)

This code is retrieving an entire table by the call to get_last_scans() then you are sorting that result inside python code! Databases have very fast built in sort functionality. Please use it.

There are plenty of other functions like this in this code. You will have to fix each of them. Do the filtering and sorting in the database. Not in your python code.