Černá magie testů výkonnosti

Většina vývojářů věří, že je dobrá v ladění výkonu, ale obecně se ukazuje, že většina vývojářů v tom dobrá není.
Kirk Pepperdine

Tento citát z rozhovoru s Kirkem Pepperdinem jsem tu použil záměrně. Chci tu psát o svých nedávných zkušenostech s měřením a laděním výkonu, ale vzhledem k tomu, že jsem docela dobrý vývojář, je velká pravděpodobnost, že jsem na to šel špatně.

Takže jste byli varováni a já se může pustit do popisu problému. Pracuji na jednom velkém a zbytečně složitém systému. Nedávno se po jedné podařené marketingové akci velká skupina zákazníku rozhodla, že ho začne volat v jeden okamžik najednou. Navíc ještě k tomu v pátek večer. No a protože to je docela velká skupina, náš systém s tím měl drobné potíže. Kolegové s řešením strávili pěkných pár dnů, nakonec našli místa potencionálních problémů a před systém postavili pár obraných mechanizmů, které ho chrání před přetížením. Nicméně problém stále trvá a bylo by pěkné ho vyřešit. Jinak by se mohlo stát, že se z velké skupiny stane malá, což by mohlo potěšit naše administrátory, ale rozhodně ne náš marketing.

Teď přicházím do hry já. Kolegové mi ukázali na jednu věc, kterou aplikace dělá evidentně zbytečně a ať prý vymyslím, jak se jí zbavit, aniž by se něco rozbilo. Jenže já jsem známý potížista a navíc jsem si vzpomněl na jednu přednášku Kirka Pepperdineho, v které říkal něco v tomto smyslu (nedaří se mi najít zdroj, takže cituji z paměti):

  1. Nejdřív si zjistěte jaký je požadovaný výkon (SLA)
  2. Naměřte současné hodnoty
  3. Zjistěte možné problémy
  4. Jeden z nich opravte
  5. Naměřte nové hodnoty
  6. Opakujte dokud nesplníte 1. nebo dokud má zákazník ještě nějaké peníze

Takže jsem si zjistil SLA a začal měřit. A už tady jsem možná udělal chybu. Začal jsem měřit na svém notebooku a ne na prostředí, které se podobá produkci. Mojí výmluvou je to, že jsem si myslel, že už víme v čem je problém. A opravdu, při testech se projevovala potíž v té jedné nadbytečné operaci. Zbytečně se tam zapisovala data do jedné tabulky. Z té se obratem zase četlo a občas tu jeden select zabíral desítky sekund. Cože? Desítky sekund u dotazu nad tabulkou v které je jen pár set tisíc záznamů? Něco je špatně. Po chvíli pátrání jsem objevil, že někteří klienti mají v dané tabulce stovky záznamů místo očekávaného tuctu a pro ně trvá dotaz neúnosně dlouho. Jeden dobře mířený index nad tabulkou, která na daný dotaz zdánlivě neměla vliv to vyřešil.

Tento problém moji pozornost obrátil na test samotný. Jak je možné, že data vypadají jinak, než je očekáváno? Ukázalo se, že chyby mohou být i v testu samotném. Místo simulace tisíců různých klientů, simuloval tisíce přístupů od pár desítek klientů. Takže den testování v čudu, ale jsme alespoň o krok blíž. Máme opravený test.

Spustím opravené testy a aplikace je stále podezřele pomalá. Navíc i když spouštím JMeter, aplikaci i databázi na jednom stroji, zátěž procesoru dosahuje ubohých 75%. Brzdou je něco jiného. Vypadá to, že disk. A opravdu MySql proces zapisuje a čte jak divoký. Že by to přece jen byly ty zbytečné inzerty. To se zjistí velice snadno, prostě je vyhodím.

Po pár minutách hledání nejlepšího místa pro sabotáž, vyřazení inzertů z boje a desítkách minut buildu a startu serveru nedočkavě spouštím test a … chvilka napětí … jste tak napjatí jako já ? … vážně ? … výsledky jsou srovnatelné s předchozími. Obě jádra se pořád flákají, disk maká jak afroameričan.

Takže dalších pár hodinek pátrání a nacházím tabulku, která na první pohled s testem moc nesouvisí. Ale předchozí testy do ní nasypaly hromadu dat, která teď zdržují. Vymažu obsah tabulky, znovu spustím test a graf výkonu málem proletěl vrškem monitoru. Požadovaný výkon bych možná splnil i na notebooku!

Problém vyřešen. Nebo ne? Co se stane, když inzerty vrátím? Další desítky minut strávené novým buildem a spouštím test. Sakra, je to srovnatelně rychlé, jako když tam ty inzerty nejsou.

Ukazuje se, že obsah databáze a způsob testování má na výkon větší vliv, než struktura kódu! To je trochu nečekaný výsledek. Jediné co jsem udělal bylo, že jsem opravil test, přidal jeden index a promazal tabulky. A už to mělo za následek dvojnásobné zvýšení výkonu. Alespoň toho naměřeného. Takže jsem tam kde jsem začal. O tom kde nás tlačí bota nevím skoro nic. Že by měl Kirk pravdu? Že by programátoři byli opravdu špatnými testery? Tomu se mi nechce věřit. Nevzdávám to a zítra se vrhnu na testy v reálném prostředí. To by v tom byl čert, abych na něco nepřišel.

Test coverage – interesting only when low

What a nice and controversial title. But I really mean it. Level of test coverage is really interesting only when it’s low. Of course, we should aim for highest coverage possible, but the higher the coverage is, the less useful information it brings. Let take the following example and let’s assume there are no tests for this method.

	/**
	 Adds positive numbers. If one of the parameters is negative,
	 IllegalArgumentException is thrown.
	 * @param a
	 * @param b
	 * @return
	 */
	public int addPositive(int a, int b)
	{
		if ((a<0) || (b<0))
		{
			throw new IllegalArgumentException("Argument is negative");
		}
		return a+a;
	}

No coverage - wow, that's interesting

The fact that some piece of code is not covered at all is really interesting. It either means, that we have to start writing tests at once or that the piece of code has to be deleted. Either way we know that we should do something. Moreover, we know exactly what to do. For example, we can start with the following test.

	@Test
	public void testAddition()
	{
		assertEquals(2, math.addPositive(1, 1));
	}

It will get us partial coverage like this
Partial coverage

Partial coverage - cool, I know what to test next

If I have partial coverage, I can see what other tests I am supposed to write. In our example I see that I have to test what happens if I pass in a negative numbers.

	@Test(expected=IllegalArgumentException.class)
	public void testNegative1()
	{
		math.addPositive(-1, 1);
	}
	@Test(expected=IllegalArgumentException.class)
	public void testNegative2()
	{
		math.addPositive(1, -1);
	}

Total coverage - I have no clue

Now we have 100% coverage. Well done! Unfortunately, I have no clue what to do next. I have no idea if I need to write more tests, I do not know if I have complete functional coverage. I have to stop looking at the coverage and I have to start thinking. You see, when the coverage was low it gave me lot of useful information, when it's high I am left on my own. For example I can think hard and come up with following complex test scenario.

	@Test
	public void testComplicatedAddition()
	{
		assertEquals(3, math.addPositive(1, 2));
	}

Please note, that the coverage was already 100% before I have written the test. So one more test could not make it higher. Nevertheless the test actually detected a bug that was there from the beginning. It means that 100% code coverage is not enough, we have to aim higher. We should care about functional coverage and not about code coverage!

That's also one of many reasons why we should do test driven development. Or at least test first development. If we write the tests before the code, we are forced to think from the beginning. In TDD we do not care about the coverage. We just think about the functionality, write corresponding tests and then add the code. Total coverage is automatically achieved.

In the opposite situation, when we write the code first and then add the tests as an afterthought, we are not forced to think. We can just write some tests, achieve the target coverage and be happy. Test coverage can be really deceitful. It can give us false sense of security. So please remember, test coverage is a good servant but a bad master.

Spring WS Test

Last few weeks I have been working on one of my pet projects. Its name is Spring WS Test. As the name implies, its main purpose is to simplify Spring WS tests.

Again, I am scratching my own itch. I am quite test infected and I have needed something that allows me to write functional tests of my application without having to depend on an external server. Until now, you basically had two options. This first one is to test WS client application using plain old JUnit together with a library like EasyMock. But usually this test are quite ugly and hard to read. Moreover this type of tests does not test your configuration. The second option is to create a functional test that calls an external mock service. But this solution requires you to have two JVM, its configuration is complicated and error prone.

Classical WS test

I have been looking for something in between, for something that would allow me to write functional tests using JUnit and would be able to run in the same JVM as the test. Unfortunately I have not been able to find anything similar.

Spring WS Test test

That’s the reason why I have created Spring WS Test project. It’s quite simple and easy even though I had to spent lot of my evenings getting it into a publishable state.

Basic configuration looks like this

<beans ...>
  <!-- Creates mock message sender -->
  <bean id="messageSender" class="net.javacrumbs.springws.test.MockWebServiceMessageSender"/>
  
  <!-- Injects mock message sender into WebServiceTemplate -->
  <bean class="net.javacrumbs.springws.test.util.MockMessageSenderInjector"/>
        
  <!-- Looks for responses on the disc based on the provided XPath -->
  <bean class="net.javacrumbs.springws.test.generator.DefaultResponseGeneratorFactoryBean">
     <property name="namespaceMap">
         <map>
            <entry key="soapenv" 
                value="http://schemas.xmlsoap.org/soap/envelope/"/>
            <entry key="ns" 
                value="http://www.springframework.org/spring-ws/samples/airline/schemas/messages"/>
         </map>
     </property>
     <property name="XPathExpressions">
         <list>
             <value>
                 concat(local-name(//soapenv:Body/*[1]),'/default-response.xml')
             </value>
         </list>
     </property>             
 </bean>
</beans>   

Here we have MockWebServiceMessageSender that replaces standard Spring WebServiceMessageSender. The replacement is done by MockMessageSenderInjector. The only other thing you have to do is to define ResponseGenerator. It’s main purpose is to look for files in you test classpath and return them as mock responses.

Of course it has to decide, which file to use. By default a XPath expression is used to determine the resource name. In our example it is concat(local-name(//soapenv:Body/*[1]),'/default-response.xml'). It takes name of the payload (first soap:Body child) and uses it as a directory name. File “default-response.xml” from this directory is used as the mock response. Simple isn’t it?

Of course you can define more complicated XPaths, you can use XSLT templates to generate your responses, you can validate your requests etc. More details can be found in the documentation.

Now I am looking for some end-user feedback. So please, if you are using Spring WS on the client side do not hesitate and test it. It should be stable enough to be used although there might be a bug here and there.