Improve performance of balance and statistics computation (see #889)

This commit is contained in:
Baptiste Jonglez 2021-10-23 16:42:37 +02:00 committed by zorun
parent d2c96c5bc6
commit f68263328c

View file

@ -99,27 +99,37 @@ class Project(db.Model):
return [m for m in self.members if m.activated] return [m for m in self.members if m.activated]
@property @property
def balance(self): def full_balance(self):
"""Returns a triple of dicts:
- dict mapping each member to its balance
- dict mapping each member to how much he/she should pay others
(i.e. how much he/she benefited from bills)
- dict mapping each member to how much he/she should be paid by
others (i.e. how much he/she has paid for bills)
"""
balances, should_pay, should_receive = (defaultdict(int) for time in (1, 2, 3)) balances, should_pay, should_receive = (defaultdict(int) for time in (1, 2, 3))
# for each person for bill in self.get_bills_unordered().all():
for person in self.members: should_receive[bill.payer.id] += bill.converted_amount
# get the list of bills he has to pay total_weight = sum(ower.weight for ower in bill.owers)
bills = Bill.query.options(orm.subqueryload(Bill.owers)).filter( for ower in bill.owers:
Bill.owers.contains(person) should_pay[ower.id] += (
ower.weight * bill.converted_amount / total_weight
) )
for bill in bills.all():
if person != bill.payer:
share = bill.pay_each() * person.weight
should_pay[person] += share
should_receive[bill.payer] += share
for person in self.members: for person in self.members:
balance = should_receive[person] - should_pay[person] balance = should_receive[person.id] - should_pay[person.id]
balances[person.id] = balance balances[person.id] = balance
return balances return balances, should_pay, should_receive
@property
def balance(self):
return self.full_balance[0]
@property @property
def members_stats(self): def members_stats(self):
@ -128,23 +138,13 @@ class Project(db.Model):
:return: one stat dict per member :return: one stat dict per member
:rtype list: :rtype list:
""" """
balance, spent, paid = self.full_balance
return [ return [
{ {
"member": member, "member": member,
"paid": sum( "paid": paid[member.id],
[ "spent": spent[member.id],
bill.converted_amount "balance": balance[member.id],
for bill in self.get_member_bills(member.id).all()
]
),
"spent": sum(
[
bill.pay_each() * member.weight
for bill in self.get_bills_unordered().all()
if member in bill.owers
]
),
"balance": self.balance[member.id],
} }
for member in self.active_members for member in self.active_members
] ]
@ -232,8 +232,13 @@ class Project(db.Model):
def get_bills_unordered(self): def get_bills_unordered(self):
"""Base query for bill list""" """Base query for bill list"""
# The subqueryload option allows to pre-load data from the
# billowers table, which makes access to this data much faster.
# Without this option, any access to bill.owers would trigger a
# new SQL query, ruining overall performance.
return ( return (
Bill.query.join(Person, Project) Bill.query.options(orm.subqueryload(Bill.owers))
.join(Person, Project)
.filter(Bill.payer_id == Person.id) .filter(Bill.payer_id == Person.id)
.filter(Person.project_id == Project.id) .filter(Person.project_id == Project.id)
.filter(Project.id == self.id) .filter(Project.id == self.id)
@ -572,7 +577,10 @@ class Bill(db.Model):
} }
def pay_each_default(self, amount): def pay_each_default(self, amount):
"""Compute what each share has to pay""" """Compute what each share has to pay. Warning: this is slow, if you need
to compute this for many bills, do it differently (see
balance_full function)
"""
if self.owers: if self.owers:
weights = ( weights = (
db.session.query(func.sum(Person.weight)) db.session.query(func.sum(Person.weight))
@ -587,6 +595,9 @@ class Bill(db.Model):
return self.what return self.what
def pay_each(self): def pay_each(self):
"""Warning: this is slow, if you need to compute this for many bills, do
it differently (see balance_full function)
"""
return self.pay_each_default(self.converted_amount) return self.pay_each_default(self.converted_amount)
def __repr__(self): def __repr__(self):