Repository navigation
Performance Considerations
This document outlines performance considerations for the LCFS system and strategies employed or to be considered for optimization.
- Framework: FastAPI is a high-performance Python web framework, built on Starlette and Pydantic, designed for speed and efficiency.
-
Asynchronous Operations: The backend utilizes
asynciowithSQLAlchemy(viaasyncpg) andaio-pika(for RabbitMQ). This allows for non-blocking I/O operations, significantly improving throughput for I/O-bound tasks by handling many concurrent requests efficiently. -
Caching: Redis is used as a caching layer (via
fastapi-cache2) to store frequently accessed data and reduce database load. See Caching Strategy for more details. -
Load Testing: The
backend/performance/directory contains alocustfile.py, indicating that Locust is used for load testing the backend APIs. This helps identify bottlenecks and validate performance under concurrent user load.-
Further Investigation: Review the
locustfile.pyand associatedreadme.mdinbackend/performance/to understand the specific scenarios tested and performance benchmarks.
-
Further Investigation: Review the
-
Database Optimization:
-
Query Efficiency: Ensure SQLAlchemy queries are optimized. Use
EXPLAIN ANALYZEfor complex queries to understand their execution plans. -
Indexing: Proper database indexing on frequently queried columns is crucial. This should be reviewed based on query patterns.
-
Connection Pooling: SQLAlchemy uses connection pooling by default, which is efficient.
-
Eager-loading conventions (repository layer, see #4099 and #4101):
-
joinedload()only for many-to-one / one-to-one relationships (organization, period, status, summary, fuel type, fuel code, ...). These cannot multiply the parent rows. - Collections (
ComplianceReport.history, the schedule collections,FuelCode.*_transport_modes) are loaded withselectinload(). Joining a collection repeats every parent column once per child row; joining two collections on the same query produces a cartesian product.ComplianceReportRepository._get_history_load_option()is the shared history loader. - When a query already joins a table for its
WHERE/ORDER BY, load the relationship from that join withcontains_eager()instead of lettingjoinedload()add a second aliased copy (seeget_assessed_compliance_report_by_period,get_organization_fuel_supply_paginated). - Filtering on a related table without joining it makes SQLAlchemy add the table as a bare
FROMentry (a cross join).get_fuel_code_by_code_prefixhad this bug; always add the explicitjoin(). - The reference-data collections on
FuelType/FuelCategory(energy density, EER, additional and target CI) are served to forms byget_fuel_supply_table_optionsand are not loaded onFuelSupplyrows. Loading a single fuel supply with those collections joined produced ~11.6 million rows on a development database. - Watch for
lazy="selectin"on the mapping:FuelType.default_carbon_intensitiesandFuelCategory.category_carbon_intensitiesare loaded that way, so every query that populates a fuel type or fuel category fires one extraSELECT ... INper collection unless the query overrides it. TheFuelSupplyloaders (_scalar_reference_optionsinfuel_supply/repo.py) switch both off withraiseload(): nothing reads them off a fuel supply row, andraiseload(unlikenoload) errors rather than returning an empty list if that ever changes.Organization.ci_applicationshas the same mapping-levelselectinand is still loaded on every organization fetch.
-
-
Measured effect of the above (development database: 1,496 compliance reports, 4,256 fuel supply rows; "cells" = rows x columns the driver transferred):
Query Before After get_fuel_supply_by_idtimed out (> 20 s), 6 collections joined 7 ms, 768 cells get_effective_fuel_supplies(99 rows)440 ms, 25,501 rows / 2.36 M cells 21 ms, 99 rows / 18 k cells get_changelog_data(fuel supplies)39 ms, 107 k cells 26 ms, 21 k cells get_compliance_report_chain(10 versions)6,003 cells 3,663 cells get_compliance_report_by_id1,305 cells 585 cells get_assessed_compliance_report_by_period9 joins (period, org, summary joined twice) 7 joins Single-report fetches now issue one extra
SELECT ... INfor history, so their wall time on a tiny local database is unchanged to slightly higher; the saving is proportional to history length x report width and grows with real data. -
Indexes:
compliance_reporthas single-column indexes onorganization_id,compliance_period_id,current_status_id,transaction_id,assigned_analyst_idandcompliance_report_group_uuid, plus composites(compliance_report_group_uuid, version, compliance_report_id)and(organization_id, compliance_period_id, version DESC)(the latter covers the "latest assessed report for an org/period" lookups in one index scan instead of a BitmapAnd over two indexes).
-
- Efficient Data Structures: Using Pydantic for data validation and serialization is generally efficient.
- Build Tool: Vite provides fast cold starts and Hot Module Replacement (HMR) during development, and optimized builds for production (code splitting, tree shaking, asset optimization).
-
Component Rendering (React):
-
Memoization: Use
React.memofor functional components andshouldComponentUpdateorPureComponentfor class components to prevent unnecessary re-renders. -
Virtualization: For long lists or large tables, consider using windowing libraries (e.g.,
react-windoworreact-virtualized) if AG Grid's built-in virtualization isn't sufficient or applicable elsewhere. -
Lazy Loading: Use
React.lazyand Suspense to code-split components and load them on demand, improving initial page load time.
-
Memoization: Use
- State Management: Efficient use of Zustand and React Query. React Query helps avoid redundant data fetching and manages server state effectively.
-
Bundle Size: Regularly analyze the production bundle size (e.g., using
vite-plugin-inspectorrollup-plugin-visualizer) to identify and optimize large dependencies. - Browser Caching: Ensure appropriate HTTP caching headers are set for static assets.
- Image Optimization: Serve images in optimized formats (e.g., WebP) and sizes.
- Debouncing/Throttling: For user inputs that trigger expensive operations (e.g., API calls in typeaheads), use debouncing or throttling.
- Flow Design: Efficient NiFi flow design is critical. Avoid unnecessary processing, use appropriate processors, and optimize processor configurations.
- Back Pressure: NiFi has built-in back-pressure mechanisms to prevent overwhelming downstream components or systems. Configure these appropriately.
- Concurrent Tasks: Configure the number of concurrent tasks for processors based on available resources and the nature of the task.
- Resource Allocation: Ensure NiFi, Zookeeper, and the source/target databases have adequate CPU, memory, and disk I/O resources.
- Batching: Process data in batches where appropriate to reduce overhead.
- OpenShift Resource Requests/Limits: Properly configure CPU and memory requests and limits for pods in OpenShift to ensure stable performance and efficient resource utilization.
- Network Latency: Consider network latency between services, especially between the application and external services like Keycloak or remote databases.
- CDN: For frontend static assets, using a Content Delivery Network (CDN) can significantly improve load times for geographically distributed users (though this depends on the OpenShift setup and if it's fronted by a CDN).
-
Monitoring: Implement comprehensive monitoring (e.g., using Prometheus, Grafana, or OpenShift's built-in monitoring) to track key performance indicators (KPIs) like response times, error rates, resource utilization, and queue lengths. The presence of
prometheus-fastapi-instrumentatorin the backend suggests Prometheus metrics are available. - Scalability: Design components to be scalable, particularly the backend and database, to handle increasing load. OpenShift provides mechanisms for horizontal pod autoscaling.
Performance is an ongoing concern. Regular testing, monitoring, and profiling are essential to identify and address bottlenecks as the system evolves and load changes.