/*
* Copyright 2017 ThoughtWorks, Inc.
*
* 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.
*/
package com.thoughtworks.go.server.service;
import com.thoughtworks.go.config.*;
import com.thoughtworks.go.config.materials.MaterialConfigs;
import com.thoughtworks.go.config.materials.git.GitMaterialConfig;
import com.thoughtworks.go.domain.ConfigErrors;
import com.thoughtworks.go.server.dao.DatabaseAccessHelper;
import com.thoughtworks.go.server.domain.Username;
import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult;
import com.thoughtworks.go.util.*;
import com.thoughtworks.go.util.command.CommandLine;
import com.thoughtworks.go.util.command.InMemoryStreamConsumer;
import org.apache.log4j.*;
import org.apache.log4j.spi.Filter;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.*;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import static com.thoughtworks.go.util.command.ProcessOutputStreamConsumer.inMemoryConsumer;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
@Ignore
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:WEB-INF/applicationContext-global.xml",
"classpath:WEB-INF/applicationContext-dataLocalAccess.xml",
"classpath:WEB-INF/applicationContext-acegi-security.xml"
})
public class PipelineConfigServicePerformanceTest {
private static String consoleAppenderForPerformanceTest;
private static String rollingFileAppenderForPerformanceTest;
static {
new SystemEnvironment().setProperty(GoConstants.USE_COMPRESSED_JAVASCRIPT, "false");
Logger rootLogger = Logger.getRootLogger();
rootLogger.setLevel(Level.INFO);
try {
Filter filter = new Filter() {
@Override
public int decide(LoggingEvent loggingEvent) {
return loggingEvent.getLoggerName().startsWith("com.thoughtworks.go.util.PerfTimer") || loggingEvent.getLoggerName().startsWith("com.thoughtworks.go.server.service.PipelineConfigServicePerformanceTest") ? 0 : -1;
}
};
ConsoleAppender consoleAppender = new ConsoleAppender(new PatternLayout("%d [%p|%c|%C{1}] %m%n"));
consoleAppender.addFilter(filter);
consoleAppender.setName(consoleAppenderForPerformanceTest);
RollingFileAppender rollingFileAppender = new RollingFileAppender(new PatternLayout("%d [%p|%c|%C{1}] %m%n"), "/tmp/config-save-perf.log", false);
rollingFileAppender.addFilter(filter);
rollingFileAppender.setName(rollingFileAppenderForPerformanceTest);
rootLogger.addAppender(rollingFileAppender);
rootLogger.addAppender(consoleAppender);
} catch (IOException e) {
e.printStackTrace();
}
}
@Autowired
private PipelineConfigService pipelineConfigService;
@Autowired
private GoConfigService goConfigService;
@Autowired
private GoConfigDao goConfigDao;
@Autowired
private DatabaseAccessHelper dbHelper;
@Autowired
private GoConfigMigration goConfigMigration;
@Autowired
private EntityHashingService entityHashingService;
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
private GoConfigFileHelper configHelper;
private final int numberOfRequests = 100;
private HttpLocalizedOperationResult result;
private Username user;
private static Logger LOGGER = Logger.getLogger(PipelineConfigServicePerformanceTest.class.getName());
@AfterClass
public static void removeLogger(){
LOGGER.removeAppender(consoleAppenderForPerformanceTest);
LOGGER.removeAppender(rollingFileAppenderForPerformanceTest);
}
@Before
public void setup() throws Exception {
configHelper = new GoConfigFileHelper();
dbHelper.onSetUp();
configHelper.usingCruiseConfigDao(goConfigDao).initializeConfigFile();
configHelper.onSetUp();
goConfigService.forceNotifyListeners();
File dumpDir = tempFolder.newFolder("perf-pipelineapi-test");
FileUtil.deleteDirectoryNoisily(dumpDir);
dumpDir.mkdirs();
result = new HttpLocalizedOperationResult();
user = new Username(new CaseInsensitiveString("admin"));
consoleAppenderForPerformanceTest = "ConsoleAppenderForPerformanceTest";
rollingFileAppenderForPerformanceTest = "RollingFileAppenderForPerformanceTest";
}
@Test
public void performanceTestForUpdatePipeline() throws Exception {
setupPipelines(numberOfRequests);
final ConcurrentHashMap<String, Boolean> results = new ConcurrentHashMap<>();
run(new Runnable() {
@Override
public void run() {
PipelineConfig pipelineConfig = goConfigService.getConfigForEditing().pipelineConfigByName(new CaseInsensitiveString(Thread.currentThread().getName()));
pipelineConfig.add(new StageConfig(new CaseInsensitiveString("additional_stage"), new JobConfigs(new JobConfig(new CaseInsensitiveString("addtn_job")))));
PerfTimer updateTimer = PerfTimer.start("Saving pipelineConfig : " + pipelineConfig.name());
pipelineConfigService.updatePipelineConfig(user, pipelineConfig, entityHashingService.md5ForEntity(pipelineConfig), result);
updateTimer.stop();
results.put(Thread.currentThread().getName(), result.isSuccessful());
if (!result.isSuccessful()) {
LOGGER.error(result.toString());
LOGGER.error("Errors on pipeline" + Thread.currentThread().getName() + " are : " + ListUtil.join(getAllErrors(pipelineConfig)));
}
}
}, numberOfRequests, results);
}
@Test
public void performanceTestForDeletePipeline() throws Exception {
setupPipelines(numberOfRequests);
final ConcurrentHashMap<String, Boolean> results = new ConcurrentHashMap<>();
run(new Runnable() {
@Override
public void run() {
PipelineConfig pipelineConfig = goConfigService.getConfigForEditing().pipelineConfigByName(new CaseInsensitiveString(Thread.currentThread().getName()));
pipelineConfig.add(new StageConfig(new CaseInsensitiveString("additional_stage"), new JobConfigs(new JobConfig(new CaseInsensitiveString("addtn_job")))));
PerfTimer updateTimer = PerfTimer.start("Saving pipelineConfig : " + pipelineConfig.name());
pipelineConfigService.deletePipelineConfig(user, pipelineConfig, result);
updateTimer.stop();
results.put(Thread.currentThread().getName(), result.isSuccessful());
if (!result.isSuccessful()) {
LOGGER.error(result.toString());
LOGGER.error("Errors on pipeline" + Thread.currentThread().getName() + " are : " + ListUtil.join(getAllErrors(pipelineConfig)));
}
}
}, numberOfRequests, results);
}
@Test
public void performanceTestForCreatePipeline() throws Exception {
setupPipelines(0);
final ConcurrentHashMap<String, Boolean> results = new ConcurrentHashMap<>();
run(new Runnable() {
@Override
public void run() {
JobConfig jobConfig = new JobConfig(new CaseInsensitiveString("job"));
StageConfig stageConfig = new StageConfig(new CaseInsensitiveString("stage"), new JobConfigs(jobConfig));
PipelineConfig pipelineConfig = new PipelineConfig(new CaseInsensitiveString(Thread.currentThread().getName()), new MaterialConfigs(new GitMaterialConfig("FOO")), stageConfig);
PerfTimer updateTimer = PerfTimer.start("Saving pipelineConfig : " + pipelineConfig.name());
pipelineConfigService.createPipelineConfig(user, pipelineConfig, result, "jumbo");
updateTimer.stop();
results.put(Thread.currentThread().getName(), result.isSuccessful());
if (!result.isSuccessful()) {
LOGGER.error(result.toString());
LOGGER.error("Errors on pipeline" + Thread.currentThread().getName() + " are : " + ListUtil.join(getAllErrors(pipelineConfig)));
}
}
}, numberOfRequests, results);
}
@After
public void tearDown() throws Exception {
configHelper.onTearDown();
dbHelper.onTearDown();
}
private void run(Runnable runnable, int numberOfRequests, final ConcurrentHashMap<String, Boolean> results) throws InterruptedException {
Boolean finalResult = true;
LOGGER.info("Tests start now!");
final ArrayList<Thread> threads = new ArrayList<>();
for (int i = 0; i < numberOfRequests; i++) {
Thread t = new Thread(runnable, "pipeline" + i);
threads.add(t);
}
for (Thread t : threads) {
Thread.sleep(1000 * (new Random().nextInt(3) + 1));
t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
LOGGER.error("Exception " + e + " from thread " + t);
results.put(t.getName(), false);
}
});
t.start();
}
for (Thread t : threads) {
int i = threads.indexOf(t);
if (i == (numberOfRequests - 1)) {
// takeHeapDump(dumpDir, i);
}
t.join();
}
for (String threadId : results.keySet()) {
finalResult = results.get(threadId) && finalResult;
}
assertThat(finalResult, is(true));
}
private void takeHeapDump(File dumpDir, int i) {
InMemoryStreamConsumer outputStreamConsumer = inMemoryConsumer();
CommandLine commandLine = CommandLine.createCommandLine("jmap").withArgs("-J-d64", String.format("-dump:format=b,file=%s/%s.hprof", dumpDir.getAbsoluteFile(), i), ManagementFactory.getRuntimeMXBean().getName().split("@")[0]);
LOGGER.info(commandLine.describe());
int exitCode = commandLine.run(outputStreamConsumer, "thread" + i);
LOGGER.info(outputStreamConsumer.getAllOutput());
assertThat(exitCode, is(0));
LOGGER.info(String.format("Heap dump available at %s", dumpDir.getAbsolutePath()));
}
private static abstract class ErrorCollectingHandler implements GoConfigGraphWalker.Handler {
private final List<ConfigErrors> allErrors;
public ErrorCollectingHandler(List<ConfigErrors> allErrors) {
this.allErrors = allErrors;
}
public void handle(Validatable validatable, ValidationContext context) {
handleValidation(validatable, context);
ConfigErrors configErrors = validatable.errors();
if (!configErrors.isEmpty()) {
allErrors.add(configErrors);
}
}
public abstract void handleValidation(Validatable validatable, ValidationContext context);
}
private List<ConfigErrors> getAllErrors(Validatable v) {
final List<ConfigErrors> allErrors = new ArrayList<>();
new GoConfigGraphWalker(v).walk(new ErrorCollectingHandler(allErrors) {
@Override
public void handleValidation(Validatable validatable, ValidationContext context) {
// do nothing here
}
});
return allErrors;
}
private void setupPipelines(Integer numberOfPipelinesToBeCreated) throws Exception {
String groupName = "jumbo";
String configFile = "<FULL PATH TO YOUR CONFIG FILE>";
String xml = FileUtil.readContentFromFile(new File(configFile));
xml = goConfigMigration.upgradeIfNecessary(xml);
goConfigService.fileSaver(false).saveConfig(xml, goConfigService.getConfigForEditing().getMd5());
LOGGER.info(String.format("Total number of pipelines in this config: %s", goConfigService.getConfigForEditing().allPipelines().size()));
if (goConfigService.getConfigForEditing().hasPipelineGroup(groupName)) {
((BasicPipelineConfigs) goConfigService.getConfigForEditing().findGroup(groupName)).clear();
}
final CruiseConfig configForEditing = goConfigService.getConfigForEditing();
for (int i = 0; i < numberOfPipelinesToBeCreated; i++) {
JobConfig jobConfig = new JobConfig(new CaseInsensitiveString("job"));
StageConfig stageConfig = new StageConfig(new CaseInsensitiveString("stage"), new JobConfigs(jobConfig));
PipelineConfig pipelineConfig = new PipelineConfig(new CaseInsensitiveString("pipeline" + i), new MaterialConfigs(new GitMaterialConfig("FOO")), stageConfig);
configForEditing.addPipeline(groupName, pipelineConfig);
}
goConfigService.updateConfig(new UpdateConfigCommand() {
@Override
public CruiseConfig update(CruiseConfig cruiseConfig) throws Exception {
return configForEditing;
}
});
}
}