Add indexes and pagination to improve app performance
This commit is contained in:
@@ -41,31 +41,36 @@ def manage_data():
|
||||
@login_required
|
||||
def export_data():
|
||||
import io
|
||||
from flask import Response
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
def generate_csv():
|
||||
"""Stream CSV rows to avoid loading all readings into memory."""
|
||||
# Write header
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(['Timestamp', 'Systolic', 'Diastolic', 'Heart Rate'])
|
||||
yield output.getvalue()
|
||||
output.seek(0)
|
||||
output.truncate(0)
|
||||
|
||||
# Write CSV header
|
||||
writer.writerow(['Timestamp', 'Systolic', 'Diastolic', 'Heart Rate'])
|
||||
# Stream readings in chunks using yield_per
|
||||
readings = Reading.query.filter_by(user_id=current_user.id).order_by(
|
||||
Reading.timestamp
|
||||
).yield_per(500)
|
||||
|
||||
# Write user readings to the CSV
|
||||
readings = Reading.query.filter_by(user_id=current_user.id).all()
|
||||
for reading in readings:
|
||||
writer.writerow([
|
||||
reading.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
reading.systolic,
|
||||
reading.diastolic,
|
||||
reading.heart_rate,
|
||||
])
|
||||
for reading in readings:
|
||||
writer.writerow([
|
||||
reading.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
reading.systolic,
|
||||
reading.diastolic,
|
||||
reading.heart_rate,
|
||||
])
|
||||
yield output.getvalue()
|
||||
output.seek(0)
|
||||
output.truncate(0)
|
||||
|
||||
# Convert text to bytes for `send_file`
|
||||
output.seek(0)
|
||||
response = io.BytesIO(output.getvalue().encode('utf-8'))
|
||||
output.close()
|
||||
|
||||
return send_file(
|
||||
response,
|
||||
return Response(
|
||||
generate_csv(),
|
||||
mimetype='text/csv',
|
||||
as_attachment=True,
|
||||
download_name='readings.csv'
|
||||
headers={'Content-Disposition': 'attachment; filename=readings.csv'}
|
||||
)
|
||||
@@ -10,6 +10,9 @@ from datetime import date, datetime, timedelta
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
|
||||
# Number of readings to show per page in list view
|
||||
PAGE_SIZE = 50
|
||||
|
||||
@main.route('/', methods=['GET'])
|
||||
def landing():
|
||||
return redirect(url_for('main.dashboard')) if current_user.is_authenticated else render_template('landing.html')
|
||||
@@ -29,26 +32,42 @@ def dashboard():
|
||||
start_date = request.form.get('start_date') or (first_reading and first_reading.strftime('%Y-%m-%d'))
|
||||
end_date = request.form.get('end_date') or (last_reading and last_reading.strftime('%Y-%m-%d'))
|
||||
|
||||
# Fetch filtered readings
|
||||
readings = fetch_readings(current_user.id, start_date, end_date, user_tz)
|
||||
# Pagination for list view
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
# Annotate readings with relative and localized timestamps
|
||||
annotate_readings(readings, user_tz)
|
||||
# Fetch paginated readings for the list view
|
||||
paginated = fetch_readings_paginated(current_user.id, start_date, end_date, user_tz, page, PAGE_SIZE)
|
||||
|
||||
# Generate calendar view
|
||||
week_view = generate_weekly_calendar(readings, datetime.now(user_tz), user_tz)
|
||||
month_view = generate_monthly_calendar(readings, datetime.now(user_tz), user_tz)
|
||||
# For calendar/graph/badges, fetch only current month + week readings (much smaller set)
|
||||
now = datetime.now(user_tz)
|
||||
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
month_start_utc = month_start.astimezone(utc)
|
||||
calendar_readings = fetch_readings_for_range(current_user.id, month_start_utc)
|
||||
|
||||
# Calculate stats and badges
|
||||
systolic_avg, diastolic_avg, heart_rate_avg = calculate_weekly_summary(readings)
|
||||
badges = calculate_progress_badges(readings)
|
||||
# Annotate all readings with relative and localized timestamps
|
||||
annotate_readings(paginated.items, user_tz)
|
||||
annotate_readings(calendar_readings, user_tz)
|
||||
|
||||
# Prepare graph data
|
||||
graph_data = prepare_graph_data(readings)
|
||||
# Build shared lookup for calendar views (single pass)
|
||||
readings_by_day = build_readings_by_day(calendar_readings, user_tz)
|
||||
|
||||
# Generate calendar views from the shared lookup
|
||||
week_view = generate_weekly_calendar(readings_by_day, now, user_tz)
|
||||
month_view = generate_monthly_calendar(readings_by_day, now, user_tz)
|
||||
|
||||
# Calculate weekly averages via SQL (much faster than Python)
|
||||
systolic_avg, diastolic_avg, heart_rate_avg = calculate_weekly_summary_sql(current_user.id)
|
||||
|
||||
# Badges from the paginated readings (or full set if needed)
|
||||
badges = calculate_progress_badges(paginated.items)
|
||||
|
||||
# Prepare graph data from calendar readings (current month)
|
||||
graph_data = prepare_graph_data(calendar_readings)
|
||||
|
||||
return render_template(
|
||||
'dashboard.html',
|
||||
readings=readings,
|
||||
readings=paginated.items,
|
||||
pagination=paginated,
|
||||
profile=current_user.profile,
|
||||
badges=badges,
|
||||
systolic_avg=systolic_avg,
|
||||
@@ -58,7 +77,7 @@ def dashboard():
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
month=month_view,
|
||||
week = week_view,
|
||||
week=week_view,
|
||||
date=date,
|
||||
**graph_data
|
||||
)
|
||||
@@ -75,22 +94,32 @@ def get_reading_date_range(user_id, user_tz):
|
||||
return first, last
|
||||
|
||||
|
||||
def fetch_readings(user_id, start_date, end_date, user_tz):
|
||||
"""Retrieve readings filtered by date range."""
|
||||
def fetch_readings_paginated(user_id, start_date, end_date, user_tz, page, per_page):
|
||||
"""Retrieve paginated readings filtered by date range."""
|
||||
query = Reading.query.filter_by(user_id=user_id)
|
||||
|
||||
if start_date and end_date:
|
||||
# Convert dates to the user's timezone
|
||||
start_dt = user_tz.localize(datetime.strptime(start_date, '%Y-%m-%d')).astimezone(utc)
|
||||
end_dt = user_tz.localize(datetime.strptime(end_date, '%Y-%m-%d')).astimezone(utc) + timedelta(days=1) - timedelta(seconds=1)
|
||||
|
||||
query = query.filter(
|
||||
Reading.timestamp >= start_dt,
|
||||
Reading.timestamp <= end_dt
|
||||
)
|
||||
|
||||
return query.order_by(Reading.timestamp.desc()).paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
|
||||
def fetch_readings_for_range(user_id, start_utc, end_utc=None):
|
||||
"""Fetch readings from a UTC start time onwards (for calendar/graph views)."""
|
||||
query = Reading.query.filter(
|
||||
Reading.user_id == user_id,
|
||||
Reading.timestamp >= start_utc
|
||||
)
|
||||
if end_utc:
|
||||
query = query.filter(Reading.timestamp <= end_utc)
|
||||
return query.order_by(Reading.timestamp.desc()).all()
|
||||
|
||||
|
||||
def annotate_readings(readings, user_tz):
|
||||
"""Add relative and localized timestamps to readings."""
|
||||
now = datetime.utcnow()
|
||||
@@ -98,30 +127,40 @@ def annotate_readings(readings, user_tz):
|
||||
reading.relative_timestamp = humanize.naturaltime(now - reading.timestamp)
|
||||
reading.local_timestamp = utc.localize(reading.timestamp).astimezone(user_tz)
|
||||
|
||||
def calculate_weekly_summary(readings):
|
||||
"""Calculate averages for the past week."""
|
||||
|
||||
def build_readings_by_day(readings, user_tz):
|
||||
"""Build a dict mapping dates to readings (single pass, shared by calendar views)."""
|
||||
readings_by_day = defaultdict(list)
|
||||
for reading in readings:
|
||||
local_date = reading.local_timestamp.date() if hasattr(reading, 'local_timestamp') else utc.localize(reading.timestamp).astimezone(user_tz).date()
|
||||
readings_by_day[local_date].append(reading)
|
||||
return readings_by_day
|
||||
|
||||
|
||||
def calculate_weekly_summary_sql(user_id):
|
||||
"""Calculate weekly averages using SQL aggregation (single DB query)."""
|
||||
one_week_ago = datetime.utcnow() - timedelta(days=7)
|
||||
weekly_readings = [r for r in readings if r.timestamp >= one_week_ago]
|
||||
if weekly_readings:
|
||||
return (
|
||||
round(sum(r.systolic for r in weekly_readings) / len(weekly_readings), 1),
|
||||
round(sum(r.diastolic for r in weekly_readings) / len(weekly_readings), 1),
|
||||
round(sum(r.heart_rate for r in weekly_readings) / len(weekly_readings), 1),
|
||||
)
|
||||
result = db.session.query(
|
||||
func.round(func.avg(Reading.systolic), 1).label('sys_avg'),
|
||||
func.round(func.avg(Reading.diastolic), 1).label('dia_avg'),
|
||||
func.round(func.avg(Reading.heart_rate), 1).label('hr_avg'),
|
||||
).filter(
|
||||
Reading.user_id == user_id,
|
||||
Reading.timestamp >= one_week_ago
|
||||
).first()
|
||||
|
||||
if result and result.sys_avg is not None:
|
||||
return float(result.sys_avg), float(result.dia_avg), float(result.hr_avg)
|
||||
return 0, 0, 0
|
||||
|
||||
def generate_monthly_calendar(readings, selected_date, local_tz):
|
||||
"""Generate a monthly calendar view."""
|
||||
|
||||
def generate_monthly_calendar(readings_by_day, selected_date, local_tz):
|
||||
"""Generate a monthly calendar view from pre-built readings_by_day."""
|
||||
today = datetime.now(local_tz).date()
|
||||
first_day = selected_date.replace(day=1)
|
||||
start_date = first_day - timedelta(days=(first_day.weekday() + 1) % 7)
|
||||
end_date = start_date + timedelta(days=41)
|
||||
|
||||
readings_by_day = defaultdict(list)
|
||||
for reading in readings:
|
||||
local_date = reading.timestamp.astimezone(local_tz).date()
|
||||
readings_by_day[local_date].append(reading)
|
||||
|
||||
return [
|
||||
{
|
||||
'day': current_date.day,
|
||||
@@ -132,23 +171,14 @@ def generate_monthly_calendar(readings, selected_date, local_tz):
|
||||
for current_date in (start_date + timedelta(days=i) for i in range((end_date - start_date).days + 1))
|
||||
]
|
||||
|
||||
def generate_weekly_calendar(readings, selected_date, local_tz):
|
||||
"""Generate a weekly calendar view."""
|
||||
# Get the start of the week (Monday) and the end of the week (Sunday)
|
||||
def generate_weekly_calendar(readings_by_day, selected_date, local_tz):
|
||||
"""Generate a weekly calendar view from pre-built readings_by_day."""
|
||||
today = datetime.now(local_tz).date()
|
||||
start_of_week = selected_date - timedelta(days=selected_date.weekday())
|
||||
end_of_week = start_of_week + timedelta(days=6)
|
||||
|
||||
# Group readings by day
|
||||
readings_by_day = defaultdict(list)
|
||||
for reading in readings:
|
||||
local_date = reading.timestamp.astimezone(local_tz).date()
|
||||
readings_by_day[local_date].append(reading)
|
||||
|
||||
# Build the weekly calendar view
|
||||
return [
|
||||
{
|
||||
'date': current_date.strftime('%a, %b %d') ,
|
||||
'date': current_date.strftime('%a, %b %d'),
|
||||
'is_today': current_date == today,
|
||||
'readings': readings_by_day.get(current_date.date(), []),
|
||||
}
|
||||
@@ -167,22 +197,21 @@ def prepare_graph_data(readings):
|
||||
def calculate_progress_badges(readings):
|
||||
"""Generate badges based on user activity and milestones."""
|
||||
now = datetime.utcnow().date()
|
||||
streak_count, badges = 1, []
|
||||
badges = []
|
||||
|
||||
if not readings:
|
||||
return badges
|
||||
|
||||
sorted_readings = sorted(readings, key=lambda r: r.timestamp.date())
|
||||
|
||||
# Use reversed() instead of re-sorting — readings come in desc order from DB
|
||||
sorted_readings = list(reversed(readings))
|
||||
streak_count = 1
|
||||
daily_streak = True
|
||||
|
||||
# Start with the first reading
|
||||
previous_date = sorted_readings[0].timestamp.date()
|
||||
|
||||
for reading in sorted_readings[1:]:
|
||||
current_date = reading.timestamp.date()
|
||||
|
||||
# Check for consecutive daily streaks
|
||||
if (current_date - previous_date).days == 1:
|
||||
streak_count += 1
|
||||
elif (current_date - previous_date).days > 1:
|
||||
@@ -190,7 +219,6 @@ def calculate_progress_badges(readings):
|
||||
|
||||
previous_date = current_date
|
||||
|
||||
# Add streak badges
|
||||
if daily_streak and streak_count >= 1:
|
||||
badges.append(f"Current Streak: {streak_count} Days")
|
||||
if daily_streak and streak_count >= 7:
|
||||
|
||||
@@ -67,7 +67,9 @@ def profile_image(user_id):
|
||||
response.headers.set('Content-Type', 'image/jpeg')
|
||||
response.headers.set('Cache-Control', 'public, max-age=86400') # Cache for 1 day
|
||||
response.headers.set('ETag', str(hash(profile.profile_pic))) # Unique ETag for the image
|
||||
response.headers.set('Last-Modified', http_date(datetime.utcnow().timestamp()))
|
||||
# Use actual profile update time instead of utcnow() which defeats caching
|
||||
last_modified = profile.updated_at or datetime.utcnow()
|
||||
response.headers.set('Last-Modified', http_date(last_modified.timestamp()))
|
||||
|
||||
return response
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user