Enable Dark Mode!
how-to-perform-performance-testing-in-odoo-19.jpg
By: Noorjahan NA

How to Perform Performance Testing in Odoo 19

Technical Odoo 19 Odoo Enterprises Odoo Community

While developing an Odoo module, ensuring that the functionality works correctly is only the first step. A customization may work without any issues with a small amount of data but become slow as the database grows. For example, a custom method that processes 20 records may take less than a second, while the same method might take several minutes when processing 20,000 records. This is why performance testing is important in Odoo development. Performance testing helps developers identify slow operations, unnecessary database queries, inefficient ORM usage, and code that does not scale well with larger datasets. 

In Odoo 19, we can use the built-in profiler and testing utilities to investigate and prevent these problems. In this blog, we are going to explain different methods of performance testing and its importance.

What Is Performance Testing?

Performance testing is the process of measuring how efficiently an Odoo operation performs under a specific workload.

Some of the common things we measure are:

  • Execution time
  • Number of SQL queries
  • Database query performance
  • Python execution time
  • ORM operations
  • Batch processing efficiency

Consider the following example:

for partner in partners:
    orders = self.env['sale.order'].search([
        ('partner_id', '=', partner.id)
    ])

While analysing this code, we can see that it is functionally correct, but it performs a search for every partner.

If there are 10 partners, the problem may not be noticeable. But if there are 10,000 partners, the same implementation can result in a large number of database queries.

Performance testing helps us identify such problems before they become production issues.

Why Performance Testing Is Important in Odoo

Odoo is an ERP system where many operations are interconnected.

A single user action can trigger:

User Action > ORM > Business Logic > Computed Fields > SQL Queries > PostgreSQL

A small performance problem in custom code can therefore affect the overall user experience.

This is particularly important for all the modules like Sales, Purchase, Inventory, Accounting, Manufacturing, etc. Because most of the Odoo modules are interconnected.

Performance also becomes more important as the number of records increases.

A solution that works well for a small database should ideally continue to perform reasonably when the database becomes much larger.

Odoo 19 Profiler

Odoo 19 provides a built-in profiler that can be used to investigate application performance.

The profiler can collect information about:

  • SQL queries
  • Python execution
  • QWeb rendering
  • Function calls

The collected information can then be analyzed through Odoo's profiling interface.

For example, if confirming a Sales Order takes several seconds, instead of immediately changing the code, we can first profile the operation and determine where the time is being spent.

The profiler is particularly useful when the cause of the performance problem is not obvious.

SQL Collector

The SQL Collector records SQL queries executed during an operation.

This is useful for finding problems such as:

  • Too many SQL queries
  • Repeated searches
  • N+1 query problems
  • Unnecessary database access

For example:

with Profiler(collectors=['sql']):
    self._process_orders()

Periodic Collector

The Periodic Collector periodically records the execution stack. It helps us to identify the Python code that consumes a significant amount of execution time. For example:

with Profiler(collectors=['traces_async'], db=None) as res:
    self._process_orders()

The profiler can then help identify which methods are consuming most of the execution time. Unlike the SQL Collector, which focuses on database queries, the Periodic Collector is useful when the bottleneck is primarily in Python code.

Query Count Testing

Performance does not always mean measuring execution time. Another useful metric is the number of SQL queries generated by an operation.

Odoo provides assertQueryCount() for this purpose.

For example:

def test_process_orders(self):
    with self.assertQueryCount(20):
        self.orders.action_process_orders()

This allows us to find out the expected query count for an operation. If a future code change increases the number of queries significantly, the test can fail and highlight the regression. This is particularly useful for custom modules which may contain additional searches inside loops.

Example: The N+1 Query Problem

One of the most common performance problems in Odoo development is the N+1 query problem.

Consider:

for partner in partners:
    orders = self.env['sale.order'].search([
        ('partner_id', '=', partner.id)
    ])
    partner.order_count = len(orders)

If there are 1,000 partners, the code can perform approximately 1,000 searches. The problem is not that search() is inherently slow. The problem is that the same database operation is being repeated unnecessarily. Instead, we can retrieve the required orders in a batch:

orders = self.env['sale.order'].search([
    ('partner_id', 'in', partners.ids)
])

We can then group the results:

order_count = {}
for order in orders:
    partner_id = order.partner_id.id
    order_count[partner_id] = order_count.get(partner_id, 0) + 1

Finally:

for partner in partners:
    partner.order_count = order_count.get(partner.id, 0)

The exact query count depends on the surrounding code and ORM behaviour, but the important improvement is that we no longer perform a separate search for every partner.

Batch Processing

Another important way of improvement for performance in Odoo is batch processing.

For example:

for record in records:
    record.write({
        'state': 'done',
    })

If we need to write the same value to every record, it is better to work with the complete recordset:

records.write({
    'state': 'done',
})

Recordsets are an important part of Odoo's ORM and are designed to allow operations to be performed on multiple records.

The same approach can be applied to scheduled actions.

Instead of loading and processing every pending record:

records = self.search([
    ('state', '=', 'pending')
])
for record in records:
    record.process()

We can process a limited batch:

records = self.search([
    ('state', '=', 'pending')
], limit=500)
for record in records:
    record.process()

This prevents a scheduled action from trying to process an extremely large number of records in a single operation.

Avoid Searches Inside Loops

Consider another common example:

for line in order.order_line:
    product = self.env['product.product'].search([
        ('default_code', '=', line.product_code)
    ], limit=1)

If an order contains 500 lines, this code may perform 500 searches. A better solution is to collect the product codes first:

codes = order.order_line.mapped('product_code')
products = self.env['product.product'].search([
    ('default_code', 'in', codes)
])

Then, create a dictionary:

product_by_code = {
    product.default_code: product
    for product in products
}

The processing loop becomes:

for line in order.order_line:
    product = product_by_code.get(line.product_code)

This approach reduces repeated database access and becomes much more scalable when the number of order lines increases.

Performance Testing with Large Datasets

A performance test should use a realistic amount of data. Testing with a smaller number of records does not tell us much about how the code behaves with a large number of records.

For example:

partners = self.env['res.partner'].create([
    {
        'name': f'Test Customer {index}',
    }
    for index in range(1000)
])

We can then execute the operation against those records:

with self.profile():
    self._process_partners(partners)

This gives us a better understanding of how the implementation behaves at scale.

It is also useful to test different dataset sizes:

10 records

100 records

1,000 records

10,000 records

The goal is to understand how execution time and query count change as the dataset grows.

Performance Testing Computed Fields

Computed fields are also considered during performance testing.

For example:

@api.depends('order_ids')
def _compute_order_count(self):
    for partner in self:
        partner.order_count = self.env['sale.order'].search_count([
            ('partner_id', '=', partner.id)
        ])

This may work correctly, but the computation performs a search for each partner. If a large number of partners are recomputed, the method can become expensive.

When writing computed fields, we need to consider the following points to improve the performance:

  • How frequently is the field recomputed?
  • How many records are processed?
  • Does the computation perform database queries?
  • Can the calculation be performed in batches?

Computed fields are sometimes responsible for performance issues that are not immediately visible because they may be triggered indirectly by other operations.

Profiling a Test

Performance profiling can also be integrated into automated tests.

For example:

def test_process_orders_performance(self):
    with self.profile():
        with self.assertQueryCount(50):
            self.orders.action_process_orders()

The profiler helps us to identify where time is being spent, while assertQueryCount() helps ensure that the number of database queries remains within an expected range. This is useful for performance regression testing. If a later code change introduces unnecessary database calls, the test can expose the problem.

Performance testing is not only about speed,

Suppose we have two implementations:

Implementation A

10,000 records

2 seconds

5,000 SQL queries

and:

Implementation B

10,000 records

2.5 seconds

30 SQL queries

At first glance, implementation A appears faster. But while evaluating, we can find that implementation B may scale much better when the database becomes larger. This is why performance testing should consider more than execution time.

Useful metrics include:

  • Execution time
  • SQL query count
  • Database query duration
  • CPU usage
  • Memory consumption
  • Number of records processed

The right metric depends on the type of operation being tested.

Common Performance Mistakes

Some performance problems appear repeatedly in custom Odoo development.

  • Searching inside loops
for record in records:
self.env['x.model'].search(...
  • Creating records individually
for values in values_list:
    self.env['x.model'].create(values)

Even though batch creation creates records individually, a common mistake is found during development.

  • Unnecessary writes
  • Repeated write() operations can trigger recomputations, tracking, automation, and other ORM operations.

  • Loading huge recordsets
  • A scheduled action that loads hundreds of thousands of records into one transaction can consume significant resources.

  • Expensive computed fields
  • Computed fields that repeatedly execute searches can become costly.

  • Calling external APIs inside loops

For example:

for partner in partners:
    requests.get(api_url)

If an external API supports batch operations, those should be considered instead.

A Simple Performance Testing Workflow

When an Odoo feature is slow, the following approach is useful:

Reproduce the Problem > Profile the Operation > Check SQL Queries > Identify the Bottleneck > Optimize the Code > Run Functional Tests > Run Performance Tests Again

The important point is to measure before optimizing. Changing code based only on assumptions can sometimes make the implementation more complicated without actually improving performance.

The best practices we need to follow while developing custom modules in Odoo 19:

  • Use recordsets effectively.
  • Avoid searches inside loops.
  • Batch database operations whenever possible.
  • Use the profiler to identify bottlenecks.
  • Monitor SQL query counts.
  • Test with realistic datasets.
  • Pay attention to computed fields.
  • Process scheduled actions in batches.
  • Avoid unnecessary writes.
  • Use raw SQL only when there is a clear performance or technical reason.
  • Add performance tests for critical operations.

Performance testing is an important part of Odoo development, especially for custom modules that will work with large amounts of business data. A method that works correctly with a small dataset is not necessarily a scalable implementation. Odoo 19 provides developers with useful tools such as the profiler, SQL Collector, Periodic Collector, and assertQueryCount() to investigate and monitor application performance. The most common performance improvements usually come from simple changes: avoiding repeated searches, processing records in batches, reducing unnecessary database operations, and making better use of the ORM. The objective of performance testing is not to make every operation execute in the minimum possible time. It is to ensure that the application continues to perform reliably as the amount of data and number of users increase. Development performance should therefore be considered alongside functionality and correctness – not after them.

To read more about How to Perform Load Testing in Odoo 19, refer to our blog, How to Perform Load Testing in Odoo 19.


Frequently Asked Questions

Is performance testing necessary for every Odoo module?

Not every module requires extensive performance testing. However, it is strongly recommended for modules that process large datasets, perform complex computations, run scheduled actions, or introduce custom database operations. For small customizations, basic query and execution checks may be sufficient.

Is execution time enough to measure Odoo performance?

No. Execution time is useful, but it should be considered together with SQL query count, database performance, and the amount of data being processed. A method may be fast in a small development database but perform poorly when the production database is much larger.

How much test data should be used?

There is no fixed number. The dataset should represent the environment in which the module is expected to operate.

If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



WhatsApp