Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
db1beb1
feat: add matrix benchmark enums
May 25, 2025
ed95708
fix: Update Loadtests and config to reflect matrix properly
May 25, 2025
1fed439
feat: add testing for load test
May 25, 2025
d3c471a
feat: add matrix benchmark enums
May 25, 2025
565d657
fix: Update Loadtests and config to reflect matrix properly
May 25, 2025
4cdb54e
feat: add testing for load test
May 25, 2025
9129611
Merge branch 'fix/matrix_load_tests' of https://github.com/GIScience/…
Jun 1, 2025
9695ebb
fix: address sonarqube and copilot PR comments
Jun 1, 2025
f9e23e3
Merge branch 'main' into fix/matrix_load_tests
HendrikLeuschner Jun 1, 2025
46341ca
fix: fix csv reading structure
Jun 1, 2025
48f3d0f
Merge branch 'fix/matrix_load_tests' of https://github.com/GIScience/…
Jun 1, 2025
eaf55d9
feat: add matrix benchmark enums
May 25, 2025
9f34046
fix: Update Loadtests and config to reflect matrix properly
May 25, 2025
ab8fc26
feat: add testing for load test
May 25, 2025
c9c62de
fix: address sonarqube and copilot PR comments
Jun 1, 2025
1c20680
fix: fix csv reading structure
Jun 1, 2025
cba5e06
fix: Address sonarqube issues
Jun 9, 2025
b866c25
fix: remove profiles other than driving-car
Jun 9, 2025
f992230
Merge branch 'fix/matrix_load_tests' of https://github.com/GIScience/…
Jun 9, 2025
fcbedfb
Merge branch 'main' into fix/matrix_load_tests
HendrikLeuschner Jun 9, 2025
1a7fe2b
feat: change matrix generation, use single coordinate as seed
Jun 19, 2025
ea1bc9c
fix: use correct destination indices
Jun 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ public class BenchmarkEnums {

// Constant for preference
public static final String PREFERENCE = "preference";
public static final String METRICS = "metrics";
public static final String DURATION = "duration";
// Constant for recommended
public static final String RECOMMENDED = "recommended";
public static final String OPTIONS = "options";


public enum TestUnit {
DISTANCE,
Expand Down Expand Up @@ -47,8 +51,8 @@ public Map<String, Object> getRequestParams() {
return switch (this) {
case ALGO_CH -> Map.of(PREFERENCE, RECOMMENDED);
case ALGO_CORE -> Map.of(PREFERENCE,
RECOMMENDED, "options", Map.of("avoid_features", List.of("ferries")));
case ALGO_LM_ASTAR -> Map.of(PREFERENCE, RECOMMENDED, "options",
RECOMMENDED, OPTIONS, Map.of("avoid_features", List.of("ferries")));
case ALGO_LM_ASTAR -> Map.of(PREFERENCE, RECOMMENDED, OPTIONS,
Map.of("avoid_polygons",
Map.of("type", "Polygon", "coordinates",
List.of(List.of(List.of(100.0, 100.0), List.of(100.001, 100.0),
Expand All @@ -73,4 +77,48 @@ public String getValue() {
}
}

/**
* Enum representing different matrix modes for benchmarking.
* getRequestParams() provides the parameters to trigger each matrix algorithm.
*/
public enum MatrixModes {
ALGO_DIJKSTRA_MATRIX,
ALGO_CORE_MATRIX,
ALGO_RPHAST_MATRIX;

public static MatrixModes fromString(String value) {
return switch (value.toLowerCase()) {
case "algodijkstra" -> ALGO_DIJKSTRA_MATRIX;
case "algocore" -> ALGO_CORE_MATRIX;
case "algorphast" -> ALGO_RPHAST_MATRIX;
default -> throw new IllegalArgumentException("Invalid matrix mode: " + value);
};
}

public List<String> getProfiles() {
return switch (this) {
case ALGO_DIJKSTRA_MATRIX, ALGO_CORE_MATRIX, ALGO_RPHAST_MATRIX -> List.of("driving-car");
};
}
/**
* Returns the request parameters for the matrix algorithm.
* These parameters are used to trigger the specific matrix algorithm.
* This is not great as we have to maintain this in multiple places,
* at the moment this is the only way to trigger the algorithms.
* What would be better is to have a common interface for the algorithms,
* but that would require a larger refactor of the codebase.
* @return a map of request parameters
*/
public Map<String, Object> getRequestParams() {
return switch (this) {
case ALGO_RPHAST_MATRIX -> Map.of(METRICS, List.of(DURATION));
case ALGO_CORE_MATRIX -> Map.of(METRICS,
List.of(DURATION), OPTIONS, Map.of("dynamic_speeds", "true"));
case ALGO_DIJKSTRA_MATRIX -> Map.of(METRICS,
List.of(DURATION), OPTIONS, Map.of("dynamic_speeds", "false", "avoid_features", List.of("ferries")));

};
}
}

}
20 changes: 15 additions & 5 deletions ors-benchmark/src/main/java/org/heigit/ors/benchmark/Config.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.heigit.ors.benchmark;

import org.heigit.ors.benchmark.BenchmarkEnums.DirectionsModes;
import org.heigit.ors.benchmark.BenchmarkEnums.MatrixModes;
import org.heigit.ors.benchmark.BenchmarkEnums.TestUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -9,6 +10,7 @@
import java.util.List;

import static org.heigit.ors.benchmark.BenchmarkEnums.DirectionsModes.*;
import static org.heigit.ors.benchmark.BenchmarkEnums.MatrixModes.*;

public class Config {
private static final Logger logger = LoggerFactory.getLogger(Config.class);
Expand All @@ -28,7 +30,8 @@ public class Config {
private final boolean parallelExecution;
private final TestUnit testUnit;
private final List<String> sourceFiles;
private final List<String> modes;
private final List<String> directionsModes;
private final List<String> matrixModes;
private final List<Integer> ranges;

public Config() {
Expand All @@ -48,8 +51,8 @@ public Config() {
this.testUnit = TestUnit.fromString(getSystemProperty("test_unit", "distance"));
this.sourceFiles = parseCommaSeparatedStringToStrings(getSystemProperty("source_files", ""));
this.ranges = parseCommaSeparatedStringToInts(this.range);
this.modes = parseCommaSeparatedStringToStrings(getSystemProperty("modes", ""));
}
this.directionsModes = parseCommaSeparatedStringToStrings(getSystemProperty("modes", ""));
this.matrixModes = parseCommaSeparatedStringToStrings(getSystemProperty("matrix_modes", ""));}

private String getSystemProperty(String key, String defaultValue) {
String value = System.getProperty(key) != null ? System.getProperty(key) : defaultValue;
Expand Down Expand Up @@ -145,9 +148,16 @@ public List<Integer> getRanges() {
}

public List<DirectionsModes> getDirectionsModes() {
return modes.isEmpty() ? List.of(ALGO_CH, ALGO_CORE, ALGO_LM_ASTAR)
: modes.stream()
return directionsModes.isEmpty() ? List.of(ALGO_CH, ALGO_CORE, ALGO_LM_ASTAR)
: directionsModes.stream()
.map(DirectionsModes::fromString)
.toList();
}

public List<MatrixModes> getMatrixModes() {
return matrixModes.isEmpty() ? List.of(ALGO_DIJKSTRA_MATRIX, ALGO_CORE_MATRIX, ALGO_RPHAST_MATRIX)
: matrixModes.stream()
.map(MatrixModes::fromString)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package org.heigit.ors.benchmark;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import io.gatling.javaapi.core.PopulationBuilder;
import io.gatling.javaapi.core.ScenarioBuilder;
import io.gatling.javaapi.core.Session;
import io.gatling.javaapi.http.HttpRequestActionBuilder;
import org.heigit.ors.benchmark.BenchmarkEnums.DirectionsModes;
import org.heigit.ors.benchmark.BenchmarkEnums.MatrixModes;
import org.heigit.ors.benchmark.exceptions.RequestBodyCreationException;
import org.heigit.ors.util.SourceUtils;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
Expand All @@ -20,16 +21,38 @@
import static io.gatling.javaapi.http.HttpDsl.http;
import static io.gatling.javaapi.http.HttpDsl.status;

/**
* Load test implementation for OpenRouteService Matrix API using Gatling
* framework.
*
* This class performs load testing on the matrix endpoint by:
* - Reading matrix test data from CSV files containing coordinates, sources,
* and destinations
* - Creating HTTP requests to the /v2/matrix/{profile} endpoint
* - Testing different matrix calculation modes and routing profiles
* - Measuring response times and throughput under concurrent load
*
* The test data is expected to be in CSV format with columns:
* coordinates, sources, destinations, distances, profile
*/
public class MatrixAlgorithmLoadTest extends AbstractLoadTest {

static {
logger = LoggerFactory.getLogger(MatrixAlgorithmLoadTest.class);
}

/**
* Constructs a new MatrixAlgorithmLoadTest instance.
* Initializes the load test with configuration from the parent class.
*/
public MatrixAlgorithmLoadTest() {
super();
}

/**
* Logs configuration information specific to matrix load testing.
* Displays source files, concurrent users, and execution mode.
*/
@Override
protected void logConfigInfo() {
logger.info("Initializing MatrixAlgorithmLoadTest:");
Expand All @@ -38,33 +61,68 @@ protected void logConfigInfo() {
logger.info("- Execution mode: {}", config.isParallelExecution() ? "parallel" : "sequential");
}

/**
* Logs the type of test being performed.
*/
@Override
protected void logTestTypeInfo() {
logger.info("Testing matrix");
}

/**
* Creates test scenarios for all combinations of matrix modes, source files,
* and profiles.
*
* @param isParallel whether scenarios should be executed in parallel
* @return stream of PopulationBuilder instances for each test scenario
*/
@Override
protected Stream<PopulationBuilder> createScenarios(boolean isParallel) {
return config.getDirectionsModes().stream()
return config.getMatrixModes().stream()
.flatMap(mode -> config.getSourceFiles().stream()
.flatMap(sourceFile -> mode.getProfiles().stream()
.map(profile -> createScenarioWithInjection(sourceFile, isParallel, mode, profile))));
}

private PopulationBuilder createScenarioWithInjection(String sourceFile, boolean isParallel, DirectionsModes mode,
/**
* Creates a single test scenario with user injection configuration.
*
* @param sourceFile path to the CSV file containing test data
* @param isParallel whether the scenario runs in parallel mode
* @param mode the matrix calculation mode to test
* @param profile the routing profile to test
* @return PopulationBuilder configured with the specified parameters
*/
private PopulationBuilder createScenarioWithInjection(String sourceFile, boolean isParallel, MatrixModes mode,
String profile) {
String scenarioName = formatScenarioName(mode, profile, isParallel);
return createMatrixScenario(scenarioName, sourceFile, config, mode, profile)
return createMatrixScenario(scenarioName, sourceFile, mode, profile)
.injectOpen(atOnceUsers(config.getNumConcurrentUsers()));
}

private String formatScenarioName(DirectionsModes mode, String profile, boolean isParallel) {
/**
* Formats a descriptive name for the test scenario.
*
* @param mode the matrix calculation mode
* @param profile the routing profile
* @param isParallel whether the scenario runs in parallel
* @return formatted scenario name string
*/
private String formatScenarioName(MatrixModes mode, String profile, boolean isParallel) {
return String.format("%s - %s - %s", isParallel ? "Parallel" : "Sequential", mode, profile);
}

private static ScenarioBuilder createMatrixScenario(String name, String sourceFile, Config config,
DirectionsModes mode, String profile) {

/**
* Creates a Gatling scenario for matrix load testing.
*
* @param name descriptive name for the scenario
* @param sourceFile path to CSV file containing test coordinates
* @param mode matrix calculation mode to test
* @param profile routing profile to test
* @return ScenarioBuilder configured for matrix testing
*/
private static ScenarioBuilder createMatrixScenario(String name, String sourceFile,
MatrixModes mode, String profile) {
try {
List<Map<String, Object>> records = csv(sourceFile).readRecords();
List<Map<String, Object>> targetRecords = SourceUtils.getRecordsByProfile(records, profile);
Expand All @@ -77,7 +135,7 @@ private static ScenarioBuilder createMatrixScenario(String name, String sourceFi
return scenario(name)
.feed(targetRecords.iterator(), 1)
.asLongAs(session -> remainingRecords.decrementAndGet() >= 0)
.on(exec(createRequest(name, config, mode, profile)));
.on(exec(createRequest(name, mode, profile)));

} catch (IllegalStateException e) {
logger.error("Error building scenario: ", e);
Expand All @@ -86,43 +144,90 @@ private static ScenarioBuilder createMatrixScenario(String name, String sourceFi
}
}

private static HttpRequestActionBuilder createRequest(String name, Config config, DirectionsModes mode,
/**
* Creates an HTTP request action for the matrix API endpoint.
*
* @param name request name for identification in test results
* @param mode matrix calculation mode
* @param profile routing profile
* @return HttpRequestActionBuilder configured for matrix API calls
*/
private static HttpRequestActionBuilder createRequest(String name, MatrixModes mode,
String profile) {
return http(name)
.post("/v2/matrix/" + profile)
.body(StringBody(session -> createRequestBody(session, config, mode)))
.header("Accept", "application/json;charset=UTF-8")
.body(StringBody(session -> createRequestBody(session, mode)))
.asJson()
.check(status().is(200));
}

static String createRequestBody(Session session, Config config, DirectionsModes mode) {
/**
* Creates the JSON request body for matrix API calls from CSV session data.
*
* @param session Gatling session containing CSV row data
* @param mode matrix calculation mode providing additional parameters
* @return JSON string representation of the request body
* @throws RequestBodyCreationException if JSON serialization fails or data is missing
*/
static String createRequestBody(Session session, MatrixModes mode) {
try {
Map<String, Object> requestBody = new java.util.HashMap<>(Map.of(
"locations", createLocationsListFromArrays(session, config),
"sources", List.of(0),
"destinations", List.of(1)));
// 1) Retrieve the raw feeder values. Gatling will give us a List<String> for each column.
@SuppressWarnings("unchecked")
List<String> coordsList = (List<String>) session.get("coordinates");
@SuppressWarnings("unchecked")
List<String> sourcesList = (List<String>) session.get("sources");
@SuppressWarnings("unchecked")
List<String> destsList = (List<String>) session.get("destinations");

// 2) Fail fast if any column is missing or empty
if (coordsList == null || coordsList.isEmpty()) {
throw new RequestBodyCreationException("'coordinates' field is missing or empty in session");
}
if (sourcesList == null || sourcesList.isEmpty()) {
throw new RequestBodyCreationException("'sources' field is missing or empty in session");
}
if (destsList == null || destsList.isEmpty()) {
throw new RequestBodyCreationException("'destinations' field is missing or empty in session");
}

// 3) The first element of each List<String> is the actual JSON‐style value.
String coordinatesJson = coordsList.get(0);
String sourcesJson = sourcesList.get(0);
String destsJson = destsList.get(0);

logger.debug(
"Raw CSV values → coordinatesJson: {}, sourcesJson: {}, destsJson: {}",
coordinatesJson, sourcesJson, destsJson
);

// 4) Let Jackson parse "[[lon, lat], [lon, lat], …]" into List<List<Double>>
List<List<Double>> locations = objectMapper.readValue(
coordinatesJson, new TypeReference<List<List<Double>>>() {}
);

// 5) Similarly parse "[0, 1, 2]" into List<Integer>
List<Integer> sources = objectMapper.readValue(
sourcesJson, new TypeReference<List<Integer>>() {}
);
List<Integer> destinations = objectMapper.readValue(
destsJson, new TypeReference<List<Integer>>() {}
);

// 6) Build the request body map and merge in any extra params from MatrixModes
Map<String, Object> requestBody = new HashMap<>(Map.of(
"locations", locations,
"sources", sources,
"destinations", destinations
));
requestBody.putAll(mode.getRequestParams());

// 7) Serialize to JSON and return
return objectMapper.writeValueAsString(requestBody);
} catch (JsonProcessingException e) {
throw new RequestBodyCreationException("Failed to create request body", e);
}
}

static List<List<Double>> createLocationsListFromArrays(Session session, Config config) {
List<List<Double>> locations = new ArrayList<>();
try {
Double startLon = Double.valueOf((String) session.getList(config.getFieldStartLon()).get(0));
Double startLat = Double.valueOf((String) session.getList(config.getFieldStartLat()).get(0));
locations.add(List.of(startLon, startLat));
Double endLon = Double.valueOf((String) session.getList(config.getFieldEndLon()).get(0));
Double endLat = Double.valueOf((String) session.getList(config.getFieldEndLat()).get(0));
locations.add(List.of(endLon, endLat));
} catch (NumberFormatException e) {
String errorMessage = String.format(
"Failed to parse coordinate values in locations list at index %d. Original value could not be converted to double",
locations.size());
throw new RequestBodyCreationException("Error processing coordinates: " + errorMessage, e);
} catch (JsonProcessingException e) {
// Jackson failed to parse or serialize
throw new RequestBodyCreationException("Failed to serialize request body to JSON", e);
}
return locations;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ public class RequestBodyCreationException extends RuntimeException {
public RequestBodyCreationException(String message, Throwable cause) {
super(message, cause);
}

public RequestBodyCreationException(String message) {
super(message);
}
}
Loading
Loading