OpenMRS Performance Testing Documentation

OpenMRS Performance Testing Documentation

Performance Testing for OpenMRS

This repository contains performance testing scripts and configurations for OpenMRS using Gatling. The latest report, including performance trends, can be found at o3-performance.openmrs.org.

Introduction

This project provides a scalable and easy-to-use framework for performance testing the OpenMRS platform. Using Gatling, we simulate user load to measure system performance, identify bottlenecks, and track trends over time. The framework is designed to be extensible, allowing developers to easily add new user roles (Personas), workflows (Scenarios), and custom reports.


Simulation Presets

Simulations are configured in the OpenMRSClinic class and controlled via the simulation-config.conf file. Each test starts with 0 users and incrementally adds users in tiers. It ramps up for one minute to the next tier's user count, runs a sustained load for the tier duration, and repeats until the final tier is complete.

The following presets are available:

Preset

Tier Count

Tier Duration

User Increment per Tier

Ramp Duration

Usage

standard

6

30 min

32

1 min

Daily scheduled runs.

commit

1

1 min

20

1 min

On commits to the main branch.

pull_request

1

1 min

20

1 min

On pull requests.

dev

env

env

env

1 min

Local development, uses environment variables.

Export to Sheets

Currently, the workload is distributed between two primary personas:

  • Doctor: 50% of active users

  • Clerk: 50% of active users

image-20250819-194039.png

Development Guide

Creating Personas and Scenarios

A Persona represents a user role (e.g., Doctor, Clerk), while a Scenario defines a specific workflow that a persona performs (e.g., Patient Registration).

1. Create a Persona

To create a new persona, extend the Persona class. A persona contains a list of scenarios that its users will execute. The constructor takes a loadShare value, which determines the percentage of total users assigned to this persona.

Example: Allocating 75% of the user load to the Clerk persona.

Java
new ClerkPersona(0.75); // 75% of total users will be Clerks

 

2. Create a Scenario

To create a new scenario, extend the Scenario class. You must override the getScenarioBuilder() method to define the user actions using the Gatling DSL. Each scenario also receives a loadShare that represents the fraction of the persona's population that will execute it.

Example: A Patient Registration scenario performed by 40% of Clerks.

Java
public class PatientRegistrationScenario extends Scenario<ClerkRegistry> { public PatientRegistrationScenario(float loadShare) { super(loadShare, new ClerkRegistry()); } @Override public ScenarioBuilder getScenarioBuilder() { return scenario("Clerk - Register Patient") .exec(registry.login()) .exec(registry.openHomePage()) .exec(registry.openRegistrationPage()) .exec(registry.registerPatient()) .exec(registry.openPatientChartPage("#{patientUuid}")); } }

This scenario would be instantiated within the ClerkPersona class like this: new PatientRegistrationScenario(0.4F).

 

Load Calculation Formula 📊

The number of users executing a specific scenario is calculated as:

Total Population×Persona Load Share×Scenario Load Share

For example, with 1000 total users, a Clerk persona with a 0.75 load share, and a Patient Registration scenario with a 0.4 load share:

1000×0.75×0.4=300 users

 

Centralized Load Configuration ⚙️

All persona and scenario loadShare values are managed in the load-config.conf file. This HOCON-based configuration makes it easy to adjust load distribution without changing any Java code.


Creating Registries

A Registry is a collection of reusable actions (like logging in or opening a page) that can be shared across multiple scenarios. To create a registry, extend the Registry class. This promotes code reuse and keeps scenarios clean and readable.

Java
public class ClerkRegistry extends Registry<ClerkHttpService> { public ClerkRegistry() { super(new ClerkHttpService()); } public ChainBuilder registerPatient() { return exec( httpService.generateOMRSIdentifier(), httpService.sendPatientRegistrationRequest()); } }

Data Management and Feeders

SharedPoolFeeder – Custom Parallel Feeder 👥

To prevent data collisions in high-concurrency tests, this project uses a custom SharedPoolFeeder. It is backed by a blocking queue to ensure that each simulated user gets a unique patient UUID without conflicts.

Important: Always return the UUID to the pool using SharedPoolFeeder.returnUuid(uuid) after the scenario is complete so it can be reused.

Example Usage:

Java
// Get a unique patient UUID from the feeder .feed(SharedPoolFeeder.feeder()) .exec(registry.openAppointmentFormPage("#{patient_uuid}")) // ... other actions .exec(session -> { String uuid = session.getString("patient_uuid"); // Return the UUID to the pool for reuse SharedPoolFeeder.returnUuid(uuid); return session; });

 

Handling Dynamic Values in Sessions

In Gatling, request builders are compiled once. To use dynamic values like timestamps or fresh UUIDs at runtime, you must use a Gatling session block. This ensures the values are generated uniquely for each user's execution.

Example: Creating a request body with the current timestamp.

Java
public HttpRequestActionBuilder checkAppointmentConflicts() { return http("Check Appointment Conflicts") .post("/openmrs/ws/rest/v1/appointments/conflicts") .body(StringBody(session -> { // These values are generated dynamically for each user String startDatetime = getCurrentDateTimeAsString(); String endDatetime = getAdjustedDateTimeAsString(0, 1); // ... build JSON payload with dynamic values ... try { return new ObjectMapper().writeValueAsString(payload); } catch (JsonProcessingException e) { throw new RuntimeException(e); } })); }

Custom Reporting and Trends 📈

Beyond the standard Gatling report, this project includes custom static pages to visualize performance and response size trends over time.

Performance Trends Page

This page displays historical trends for key user actions.

  • How it works: A GitHub Action runs after every standard (daily) performance test. It parses the new Gatling report, appends the aggregated data to the request-trends.csv file in the report branch, and deploys the static site found in the /performance-trends folder.

  • Branching: The report branch holds the historical data, ensuring the main branch remains clean. The CSV file in main is only for local testing.

  • Vulnerability: This process relies on a custom parser for Gatling's simulation log. Future Gatling updates could potentially break the parser, requiring maintenance.

image-20250819-193729.png
image-20250819-193644.png

Response Size Trends Page

This page tracks the size (in KB) of HTTP responses for various requests. This helps identify unexpected increases in payload size.

  • How it works: The ResponseSizeLogger.java file hooks into Gatling's HttpProtocolBuilder to capture the size of each response and log it to a CSV file. The static site in the /response-size folder then visualizes this data.

  • Resilience: This method uses Gatling's internal APIs and is immune to changes in Gatling's reporting format, making it more robust than the performance trends parser.

image-20250819-193903.png

Gatling Report Customization

To provide a seamless user experience, the standard Gatling report is modified to include navigation to our custom trend pages.

  • The GatlingReportModifier.java file programmatically injects custom HTML and CSS into the index.html and styles.css files generated by Gatling, adding navigation links to the Performance Trends and Response Size pages.


Best Practices

Use dynamic session variables for values that must be unique per user or execution, like timestamps.

Use the SharedPoolFeeder to manage shared data like patient UUIDs and prevent conflicts.

Always return UUIDs to the SharedPoolFeeder pool to avoid data exhaustion.

Avoid hardcoding values in URLs or request bodies. Use feeders or session variables.

Do not use single quotes inside http() request names (e.g., http('My Request')). This is known to break Gatling's report generation. Use double quotes instead: http("My Request").