Merge pull request #2282 from tkyjovsk/jpa-performance

Added some jpa perf tests (adding/removing many users)
This commit is contained in:
Stian Thorgersen 2016-02-29 11:58:09 +01:00
commit d7641f8830
12 changed files with 480 additions and 29 deletions

View file

@ -17,7 +17,9 @@
package org.keycloak.testsuite.arquillian; package org.keycloak.testsuite.arquillian;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* *
@ -34,6 +36,8 @@ public final class TestContext {
private boolean adminLoggedIn; private boolean adminLoggedIn;
private final Map customContext = new HashMap<>();
public TestContext(SuiteContext suiteContext, Class testClass) { public TestContext(SuiteContext suiteContext, Class testClass) {
this.suiteContext = suiteContext; this.suiteContext = suiteContext;
this.testClass = testClass; this.testClass = testClass;
@ -88,4 +92,12 @@ public final class TestContext {
+ (isAdapterTest() ? "App server container: " + getAppServerInfo() + "\n" : ""); + (isAdapterTest() ? "App server container: " + getAppServerInfo() + "\n" : "");
} }
public Object getCustomValue(Object key) {
return customContext.get(key);
}
public void setCustomValue(Object key, Object value) {
customContext.put(key, value);
}
} }

View file

@ -43,6 +43,8 @@ public class IOUtil {
private static final Logger log = Logger.getLogger(IOUtil.class); private static final Logger log = Logger.getLogger(IOUtil.class);
public static final File PROJECT_BUILD_DIRECTORY = new File(System.getProperty("project.build.directory", "target"));
public static <T> T loadJson(InputStream is, Class<T> type) { public static <T> T loadJson(InputStream is, Class<T> type) {
try { try {
return JsonSerialization.readValue(is, type); return JsonSerialization.readValue(is, type);

View file

@ -14,15 +14,28 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.testsuite.util; package org.keycloak.testsuite.util;
import java.text.MessageFormat; import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.io.IOUtils;
import org.jboss.logging.Logger;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import static org.keycloak.testsuite.util.IOUtil.PROJECT_BUILD_DIRECTORY;
import static org.jgroups.util.Util.assertTrue;
/** /**
* *
@ -30,45 +43,129 @@ import java.util.Map;
*/ */
public class Timer { public class Timer {
private static Long time; public static final Timer DEFAULT = new Timer();
private static final Map<String, List<Long>> stats = new HashMap<>(); protected final Logger log = Logger.getLogger(Timer.class);
public static void time() { protected static final File DATA_DIR = new File(PROJECT_BUILD_DIRECTORY, "stats/data");
time = new Date().getTime(); protected static final File CHARTS_DIR = new File(PROJECT_BUILD_DIRECTORY, "stats/charts");
public static final String DEFAULT_OPERATION = "DEFAULT_OPERATION";
private Long time;
private String operation = DEFAULT_OPERATION;
private final Map<String, List<Long>> stats = new TreeMap<>();
public long elapsedTime() {
long elapsedTime = 0;
if (time == null) {
} else {
elapsedTime = new Date().getTime() - time;
}
return elapsedTime;
} }
public static void time(String operation) { public void reset() {
if (time == null) { reset(operation); // log last operation
System.out.println(MessageFormat.format("Starting timer for operation {0}", operation)); }
time();
} else { public void reset(String operation) {
long timeOrig = time; reset(operation, true);
time(); }
logOperation(operation, time - timeOrig);
System.out.println(MessageFormat.format("Operation {0} took {1} ms", operation, time - timeOrig)); public void reset(String newOperation, boolean logOperationOnChange) {
if (time != null) {
if (operation.equals(newOperation) || logOperationOnChange) {
logOperation(operation, elapsedTime());
}
}
time = new Date().getTime();
if (!operation.equals(newOperation)) {
operation = newOperation;
log.info(String.format("Operation '%s' started.", newOperation));
} }
} }
private static void logOperation(String operation, long delta) { private void logOperation(String operation, long duration) {
if (!stats.containsKey(operation)) { if (!stats.containsKey(operation)) {
stats.put(operation, new ArrayList<Long>()); stats.put(operation, new ArrayList<Long>());
} }
stats.get(operation).add(delta); stats.get(operation).add(duration);
log.info(String.format("Operation '%s' took: %s ms", operation, duration));
} }
public static void printStats() { public void clearStats() {
if (!stats.isEmpty()) { clearStats(true, true, true);
System.out.println("OPERATION STATS:"); }
}
for (String op : stats.keySet()) { public void clearStats(boolean logStats, boolean saveData, boolean saveCharts) {
long sum = 0; if (logStats) {
for (Long t : stats.get(op)) { log.info("Timer Statistics:");
sum += t; for (String op : stats.keySet()) {
long sum = 0;
for (Long duration : stats.get(op)) {
sum += duration;
}
log.info(String.format("Operation '%s' average: %s ms", op, sum / stats.get(op).size()));
}
}
if (PROJECT_BUILD_DIRECTORY.exists()) {
DATA_DIR.mkdirs();
CHARTS_DIR.mkdirs();
for (String op : stats.keySet()) {
if (saveData) {
saveData(op);
}
if (saveCharts) {
saveChart(op);
}
} }
System.out.println(MessageFormat.format("Operation {0} average time: {1,number,#} ms", op, sum / stats.get(op).size()));
} }
stats.clear(); stats.clear();
} }
private void saveData(String op) {
try {
File f = new File(DATA_DIR, op.replace(" ", "_") + ".txt");
if (!f.createNewFile()) {
throw new IOException("Couldn't create file: " + f);
}
OutputStream stream = new BufferedOutputStream(new FileOutputStream(f));
for (Long duration : stats.get(op)) {
IOUtils.write(duration.toString(), stream);
IOUtils.write("\n", stream);
}
stream.flush();
IOUtils.closeQuietly(stream);
} catch (IOException ex) {
log.error("Unable to save data for operation '" + op + "'", ex);
}
}
private void saveChart(String op) {
XYSeries series = new XYSeries(op);
int i = 0;
for (Long duration : stats.get(op)) {
series.add(++i, duration);
}
final XYSeriesCollection data = new XYSeriesCollection(series);
final JFreeChart chart = ChartFactory.createXYLineChart(
op,
"Operations",
"Duration (ms)",
data,
PlotOrientation.VERTICAL,
true,
true,
false
);
try {
ChartUtilities.saveChartAsPNG(
new File(CHARTS_DIR, op.replace(" ", "_") + ".png"),
chart, 640, 480);
} catch (IOException ex) {
log.warn("Unable to save chart for operation '" + op + "'.");
}
}
} }

View file

@ -16,6 +16,7 @@
*/ */
package org.keycloak.testsuite; package org.keycloak.testsuite;
import java.io.File;
import org.keycloak.testsuite.arquillian.TestContext; import org.keycloak.testsuite.arquillian.TestContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -36,7 +37,6 @@ import org.keycloak.admin.client.resource.RealmsResource;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.arquillian.SuiteContext; import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.auth.page.WelcomePage; import org.keycloak.testsuite.auth.page.WelcomePage;
@ -52,6 +52,8 @@ import org.keycloak.testsuite.auth.page.login.OIDCLogin;
import org.keycloak.testsuite.auth.page.login.UpdatePassword; import org.keycloak.testsuite.auth.page.login.UpdatePassword;
import org.keycloak.testsuite.util.Timer; import org.keycloak.testsuite.util.Timer;
import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.testsuite.util.WaitUtils;
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
/** /**
* *
@ -124,7 +126,6 @@ public abstract class AbstractKeycloakTest {
public void afterAbstractKeycloakTest() { public void afterAbstractKeycloakTest() {
// removeTestRealms(); // keeping test realms after test to be able to inspect failures, instead deleting existing realms before import // removeTestRealms(); // keeping test realms after test to be able to inspect failures, instead deleting existing realms before import
// keycloak.close(); // keeping admin connection open // keycloak.close(); // keeping admin connection open
Timer.printStats();
} }
private void updateMasterAdminPassword() { private void updateMasterAdminPassword() {

View file

@ -0,0 +1,52 @@
package org.keycloak.testsuite.user;
import javax.ws.rs.core.Response;
import static javax.ws.rs.core.Response.Status.CREATED;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.idm.UserRepresentation;
import static org.keycloak.testsuite.admin.ApiUtil.getCreatedId;
import static org.junit.Assert.assertEquals;
import org.keycloak.testsuite.AbstractAuthTest;
/**
*
* @author tkyjovsk
*/
public abstract class AbstractUserTest extends AbstractAuthTest {
protected UsersResource users() {
return testRealmResource().users();
}
protected UserResource user(UserRepresentation user) {
if (user.getId()==null) {
throw new IllegalStateException("User id cannot be null.");
}
return user(user.getId());
}
protected UserResource user(String id) {
return users().get(id);
}
public static UserRepresentation createUserRep(String username) {
UserRepresentation user = new UserRepresentation();
user.setUsername(username);
user.setEmail(username + "@email.test");
return user;
}
public UserRepresentation createUser(UserRepresentation user) {
return createUser(users(), user);
}
public UserRepresentation createUser(UsersResource users, UserRepresentation user) {
Response response = users.create(user);
assertEquals(CREATED.getStatusCode(), response.getStatus());
user.setId(getCreatedId(response));
response.close();
return user;
}
}

View file

@ -0,0 +1,48 @@
# Keycloak JPA Performance Tests
## How to run
1. Build the Arquilian Base Testsuite module: `/testsuite/integration-arquillian/base`
2. Run the test from this module using `mvn test` or `mvn clean test`.
Optional parameters:
```
-Dmany.users.count=10000
-Dmany.users.batch=1000
```
### With MySQL
Start dockerized MySQL:
```
docker run --name mysql-keycloak -e MYSQL_ROOT_PASSWORD=keycloak -e MYSQL_DATABASE=keycloak -e MYSQL_USER=keycloak -e MYSQL_PASSWORD=keycloak -d -p 3306:3306 mysql
```
Additional test parameters:
```
-Pclean-jpa
-Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak
-Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver
-Dkeycloak.connectionsJpa.user=keycloak
-Dkeycloak.connectionsJpa.password=keycloak
```
### With PostgreSQL
Start dockerized PostgreSQL:
```
docker run --name postgres-keycloak -e POSTGRES_PASSWORD=keycloak -d -p 5432:5432 postgres
```
Additional test parameters:
```
-Pclean-jpa
-Dkeycloak.connectionsJpa.url=jdbc:postgresql://localhost/postgres
-Dkeycloak.connectionsJpa.driver=org.postgresql.Driver
-Dkeycloak.connectionsJpa.user=postgres
-Dkeycloak.connectionsJpa.password=keycloak
```
## Reports
Test creates reports in `target/stats`.

View file

@ -0,0 +1,34 @@
<?xml version="1.0"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-tests-other</artifactId>
<version>1.9.1.Final-SNAPSHOT</version>
</parent>
<artifactId>integration-arquillian-tests-jpa-performance</artifactId>
<name>Keycloak JPA Performance Tests</name>
</project>

View file

@ -0,0 +1,119 @@
package org.keycloak.testsuite.user;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.util.Timer;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.util.JsonSerialization;
import static org.junit.Assert.fail;
import org.keycloak.admin.client.resource.RealmResource;
import static org.keycloak.testsuite.util.IOUtil.PROJECT_BUILD_DIRECTORY;
/**
*
* @author tkyjovsk
*/
public class ManyUsersTest extends AbstractUserTest {
private static final int COUNT = Integer.parseInt(System.getProperty("many.users.count", "10000"));
private static final int BATCH = Integer.parseInt(System.getProperty("many.users.batch", "1000"));
private static final String REALM = "realm_with_many_users";
private List<UserRepresentation> users;
private final Timer realmTimer = new Timer();
private final Timer usersTimer = new Timer();
protected RealmResource realmResource() {
return realmsResouce().realm(REALM);
}
@Before
public void before() {
users = new LinkedList<>();
for (int i = 0; i < COUNT; i++) {
users.add(createUserRep("user" + i));
}
realmTimer.reset("create realm before test");
RealmRepresentation realm = new RealmRepresentation();
realm.setRealm(REALM);
realmsResouce().create(realm);
}
@After
public void after() {
realmTimer.clearStats(true, true, false);
usersTimer.clearStats();
}
@Test
public void manyUsers() throws IOException {
RealmRepresentation realm = realmResource().toRepresentation();
realm.setUsers(users);
// CREATE
realmTimer.reset("create " + users.size() + " users");
usersTimer.reset("create " + BATCH + " users");
int i = 0;
for (UserRepresentation user : users) {
createUser(realmResource().users(), user);
if (++i % BATCH == 0) {
usersTimer.reset();
log.info("Created users: " + i + " / " + users.size());
}
}
if (i % BATCH != 0) {
usersTimer.reset();
log.info("Created users: " + i + " / " + users.size());
}
// SAVE REALM
realmTimer.reset("save realm with " + users.size() + " users");
File realmFile = new File(PROJECT_BUILD_DIRECTORY, REALM + ".json");
JsonSerialization.writeValueToStream(new BufferedOutputStream(new FileOutputStream(realmFile)), realm);
// DELETE REALM
realmTimer.reset("delete realm with " + users.size() + " users");
realmResource().remove();
try {
realmResource().toRepresentation();
fail("realm not deleted");
} catch (Exception ex) {
log.debug("realm deleted");
}
// RE-IMPORT SAVED REALM
realmTimer.reset("re-import realm with " + realm.getUsers().size() + " users");
realmsResouce().create(realm);
realmTimer.reset("load " + realm.getUsers().size() + " users");
users = realmResource().users().search("", 0, Integer.MAX_VALUE);
// DELETE INDIVIDUAL USERS
realmTimer.reset("delete " + users.size() + " users");
usersTimer.reset("delete " + BATCH + " users", false);
i = 0;
for (UserRepresentation user : users) {
realmResource().users().get(user.getId()).remove();
if (++i % BATCH == 0) {
usersTimer.reset();
log.info("Deleted users: " + i + " / " + users.size());
}
}
if (i % BATCH != 0) {
usersTimer.reset();
log.info("Deleted users: " + i + " / " + users.size());
}
realmTimer.reset();
}
}

View file

@ -146,6 +146,12 @@
<module>mod_auth_mellon</module> <module>mod_auth_mellon</module>
</modules> </modules>
</profile> </profile>
<profile>
<id>jpa-performance</id>
<modules>
<module>jpa-performance</module>
</modules>
</profile>
</profiles> </profiles>
</project> </project>

View file

@ -108,6 +108,7 @@
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<configuration> <configuration>
<systemPropertyVariables> <systemPropertyVariables>
<project.build.directory>${project.build.directory}</project.build.directory>
<browser>${browser}</browser> <browser>${browser}</browser>
<firefox_binary>${firefox_binary}</firefox_binary> <firefox_binary>${firefox_binary}</firefox_binary>
<shouldDeploy>false</shouldDeploy> <shouldDeploy>false</shouldDeploy>
@ -223,6 +224,12 @@
<version>2.1.0.Alpha3</version><!-- TODO upgrade <arquillian-graphene.version> and use ${arquillian-graphene.version} --> <version>2.1.0.Alpha3</version><!-- TODO upgrade <arquillian-graphene.version> and use ${arquillian-graphene.version} -->
</dependency> </dependency>
<dependency>
<groupId>jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.0.13</version>
</dependency>
<!-- <dependency> <!-- <dependency>
<groupId>org.arquillian.extension</groupId> <groupId>org.arquillian.extension</groupId>
<artifactId>arquillian-recorder-reporter-impl</artifactId> <artifactId>arquillian-recorder-reporter-impl</artifactId>
@ -411,10 +418,83 @@
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>xml-maven-plugin</artifactId> <artifactId>xml-maven-plugin</artifactId>
</plugin> </plugin>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
</plugin>
</plugins> </plugins>
</build> </build>
</profile> </profile>
<!-- MySQL -->
<profile>
<activation>
<property>
<name>keycloak.connectionsJpa.driver</name>
<value>com.mysql.jdbc.Driver</value>
</property>
</activation>
<id>mysql</id>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</profile>
<!-- PostgreSQL -->
<profile>
<activation>
<property>
<name>keycloak.connectionsJpa.driver</name>
<value>org.postgresql.Driver</value>
</property>
</activation>
<id>postgresql</id>
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
</dependencies>
</profile>
<profile>
<id>clean-jpa</id>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<configuration>
<changeLogFile>META-INF/jpa-changelog-master.xml</changeLogFile>
<url>${keycloak.connectionsJpa.url}</url>
<driver>${keycloak.connectionsJpa.driver}</driver>
<username>${keycloak.connectionsJpa.user}</username>
<password>${keycloak.connectionsJpa.password}</password>
<promptOnNonLocalDatabase>false</promptOnNonLocalDatabase>
<databaseClass>${keycloak.connectionsJpa.liquibaseDatabaseClass}</databaseClass>
</configuration>
<executions>
<execution>
<id>clean-jpa</id>
<phase>clean</phase>
<goals>
<goal>dropAll</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
<profile> <profile>
<id>auth-server-wildfly</id> <id>auth-server-wildfly</id>
<properties> <properties>