Optimal JVM parameters

boky March 29th, 2010

Since most of our software runs on JVM, we’ve had our share of experience with crashing JVMs (Out Of Any Kind Of Memory), tuning JVM and optimizing its performance. Some are on 32-bit JVMs, others on 64-bits. Some run many small applications, others run bigger applications. Some run resin, others JBoss yet there are some running Tomcat. Most are on Linux, some Windows.

But they all have some things in common:

  • they must run 24/7
  • they all run server VMs
  • they are all web applications

While there is no real way to tell which options will make your JVM work best, we’ve found the following to be helpful. I thought it would be nice to share:

-Xms1024m -Xmx1024m
Setting both Xmx and Xms to the same value will help eliminate memory fragmentation. Your application should run a bit faster, as the VM will not start at minimum and allocate additional memory as the application server / your application starts up.

-XX:MaxPermSize=512M
You will most certainly want to increase your permanent generation size, as the default is often too small. Here we’ve set it to half of assigned memory and works quite fine.

-Xmn128m
Change the new generation size. If your application is creating and releasing a lot of objects in short time (usually correlated with number of connections, usage of your website), you may find yourself with a JVM crashing when the “eden / new generation” has been exhausted. This will show up as “100% eden” in the crash log. Increasing the new generation size might help.

-Xss2m
Each thread in the JVM will get its own stack. The default stack is quite small and setting it a bit higher is usually a good idea. 2048k should be enough usually appropriate for most usage scenarios, but your mileage may vary. Increased when you have really long stacks (quite common if using a servlet container + several frameworks). Your first sign of having a too short stack will be a StackOverflowError.

-Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000
These two parameters increate garbage collection interval for RMI connections. Mostly a JBoss issue, but other app servers run just fine with this settings. Should also speed things up a bit.

-Dsun.net.inetaddr.ttl=30
Sun’s Oracle’s JVM has a “feature” which cashes all DNS lookups. Forever. This might be an issue if the some of the external services changes IP and you don’t restart your server very often (reCaptha is one such example). Set this parameter and have JVM forget lookups after a certain time.

-XX:+UseConcMarkSweepGC
Change the garbage collector. We’ve had better experiences using UseConcMarkSweepGC, especially on multi-processor machines. Although, it seems “G1” will be the best after it’s finished.

-XX:+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled
Support class unloading. Usefull even in production, as it allows you to completely undeploy the previous application if you’re doing hot-deployment. One option name is old (CMSClassUnloadingEnabled) the other new (CMSPermGenSweepingEnabled) but both still work though.

-Dfile.encoding=utf-8
Since we don’t speak US-ASCII only, we moved everything to UTF-8 several years ago. Even all our JSP pages are defined as @page encoding=”UTF-8” contentType=”..; charset=UTF-8” You would be amazed how are i18n problems just vanished. UTF-8 rules.

-Denvironment.type=prod
We “mark” our servers as “prod”, “test” and “dev”. In ideal world this would not be necessary, but sometimes the code needs to know in which environment it runs. This helps avoid confusion and have that same setting in several different places and each person inventing his own way of determening the enviornment.

-Dorg.apache.catalina.STRICT_SERVLET_COMPLIANCE=false
Tomcat goodie. Being too much servlet-compliant is not always a good thing(TM). This relaxes some strange constrains that makes applications — especially ones ported from other servlet containers — run better. Sometimes, applications will not run without this parameter at all. If you’re concerned about cross-container compatibility, leave this parameter out and develop exclusively on Tomcat. If you’re running applications developed by different teams and/or vendors, by all means, put it in.

-XX:+UseCompressedOops
Using compressed oopts will decrease your memory usage when running 64-bit JVMs. In a nutshell, it tells JVM to use 32-bit pointers around the code (where possible).

-ea
For development/staging only. Enable assertions. If you’re a prudent developer, you’re code will have a lot of sanity checks. This will enable them. This way it’s easier to spot errors before going live.

-Dcom.sun.xml.ws.transport.http.client.HttpTransportPipe.dump=true -Dcom.sun.xml.ws.transport.http.HttpAdapter.dump=true -Dcom.sun.xml.ws.util.pipe.StandaloneTubeAssembler.dump=true
For development/staging only. Enable debugging of JAX-WS /WSIT. All communication (XMLs, HTTP trafic) of webservices will be dumped to your STDOUT. If you’re having problems with webservices and don’t know what’s wrong, sometimes this is the only way to go.

If you have any other useful options, I’d be more than happy to add them to the list.

HeuristicMixedException and jBoss

boky March 18th, 2010

Seen a lot of these in your jboss logs?

[com.arjuna.ats.jta.logging.loggerI18N] [com.arjuna.ats.internal.jta.resources.arjunacore.preparefailed] [com.arjuna.ats.internal.jta.resources.arjunacore.preparefailed] XAResourceRecord.prepare - prepare failed with exception XAException.XAER_RMERR

javax.transaction.HeuristicMixedException
at com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionImple.commitAndDisassociate(TransactionImple.java:1390)
at com.arjuna.ats.internal.jta.transaction.arjunacore.BaseTransaction.commit(BaseTransaction.java:135)
at com.arjuna.ats.jbossatx.BaseTransactionManagerDelegate.commit(BaseTransactionManagerDelegate.java:87)
at org.jboss.tm.usertx.client.ServerVMClientUserTransaction.commit(ServerVMClientUserTransaction.java:140)

I’ve had this issue with jBoss and Postgres XA datasource after upgrading from Postgres 8.1 to Postgres 8.4.

Was quite puzzled by it, I even tried hacking the jbossjta-properties.xml and datasource config. Nothing worked, Google was of no help.

Later I found out that — surprisingly — reading the manuals helps.

To be exact, there’s a passage in the documentation that states:

If you are using prepared transactions, you will probably want max_prepared_transactions to be at least as large as max_connections, so that every session can have a prepared transaction pending.

By default (since 8.3) I think this parameter is set to “0” and was “5” before that. We copied an older config over and we had it at “5”.

So, since we had max_connections set to 512, I’ve changed the max_prepared_transactions to 1024, restarted the server (reload won’t suffice) and - voila - things started working.

Another one of those: “you need to remember this for the next time”.

JDBCAppender for log4j

boky February 3rd, 2010

After being quite dissatisfied with the current state of database appenders for log4j (see examples here, here and here) I’ve decided to write my own.

Features:

  • uses PreparedStatements exclusively. No more messing with queries, quotes and similar stuff
  • uses batch mode to speed up inserts
  • inserts are done in another thread to prevent delays with logging
  • uses DataSources to better manage database connectivity
  • query is database-independent (any SQL-92 compliant server should work)
  • config-compatible with Danko’s implementation
  • table name and column names can be configured
  • does not care if your table has an ID column or not
  • drops statements on SQL error

What else could be done?

  • Better handling of exceptions. We could keep logged statements (up to a point) or spill them over to another Appender.
  • Add the possibility to call a stored procedure instead of doing bulk insert

Without further ado, here’s the code. I’m releasing it under BSD license, so feel free to hack it in any way you like. But if you do make it better, please let me know so I can publish it here.

Compress your javascript and CSS

boky September 6th, 2009

Maybe some of you know of that you can speed up your speed up your application a lot, if you compress your CSSs and javascripts. Maybe some of you do it manually or have used online tools. You might have even heard of YUI compressor, which doesn’t use search&repleace but Rhino to analyze your code, making the compression process even more robust.

But, until now you always had to do it manually. Now I have a solution for you: automatic compression of your javascripts and CSSs.

I have created a simple filter that compresses the code on the fly. This makes it transparent to use and seeds up transfer times. Just make sure you cache the result through a cache filter and/or reverse proxy and/or by setting cache headers or you’ll see increased processor usage which won’t make your sysadmin happy.

Usage.

  • download the code
  • compile with Maven, if you changed anything
  • Put this in your web.xml:
<filter>
 <filter-name>CssAndJSCompressorFilter</filter-name>
 <filter-class>com.parsek.web.CssAndJSCompressorFilter</filter-class>
</filter>

<filter-mapping>
 <filter-name>CssAndJSCompressorFilter</filter-name>
 <url-pattern>*.css</url-pattern>
 <dispatcher>REQUEST</dispatcher>
</filter-mapping>

<filter-mapping>
 <filter-name>CssAndJSCompressorFilter</filter-name>
 <url-pattern>*.js</url-pattern>
 <dispatcher>REQUEST</dispatcher>
</filter-mapping>

I’m releasing the code under LGPLv3, so you are free to use it anywhere you want. If you find it useful, do mention my name and put a link to this site and leave a comment how you are using it.

Download the code.

Exporting part of your SVN tree

boky August 27th, 2009

Recently I’ve came up with a problem that required that I made part of our SVN tree accessible to an outside partner. Of course we did not want to give him access to everything. My first thought was to just use apache reverse proxy and proxy one suburl to root of the public server. Boy was I wrong.

Googling around discovered that this is at least tricky but better yet impossible to do. Therefore, my another solution involved exporting the tree under the same URL but also adding some rewriting rules to make it more user friendly. Wrong, again.

As it turns out your also need to export the virtual !svn directory to make things work. Here it goes, the final solution that seems to be working at the moment:

<VirtualHost *:443>
        ServerAdmin     admin@example.org
        ServerName      project-svn.example.net
        DocumentRoot    /store/web/proxy

        SSLEngine               on
        SSLProtocol             all -SSLv2
        SSLCipherSuite          ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW
        SSLCertificateFile      /etc/httpd/conf.d/project-svn.example.net.ssl/_.example.net.crt
        SSLCertificateKeyFile   /etc/httpd/conf.d/project-svn.example.net.ssl/_.example.net.key
        SSLCertificateChainFile /etc/httpd/conf.d/project-svn.example.net.ssl/exampleca.crt

        SetEnv          force-proxy-request-1.0 1
        SetEnv          proxy-nokeepalive       1
        SetEnvIf        User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0

        ErrorLog        "|/usr/sbin/rotatelogs /var/log/httpd/project-svn.example.net/error.443.log.%Y-%m-%d 86400"
        CustomLog       "|/usr/sbin/rotatelogs /var/log/httpd/project-svn.example.net/access.443.log.%Y-%m-%d 86400" full

        <IfModule mod_deflate.c>
            <IfModule mod_headers.c>
                Header set Accept-Encoding "gzip, deflate"
            </IfModule>
            SetInputFilter DEFLATE
            SetOutputFilter DEFLATE
            DeflateFilterNote Input instream
            DeflateFilterNote Output outstream
            DeflateFilterNote Ratio ratio
            BrowserMatch ^Mozilla/4 gzip-only-text/html
            BrowserMatch ^Mozilla/4\.0[678] no-gzip
            BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
            SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|jar|zip|rar|mpe?g)$ no-gzip dont-vary
            Header append Vary User-Agent env=!dont-vary
        </IfModule>

        ProxyPass               /svnindex.xsl                   http://svn.internal.pri/svnindex.xsl
        ProxyPassReverse        /svnindex.xsl                   http://svn.internal.pri/svnindex.xsl
        ProxyPass               /css/                           http://svn.internal.pri/css/
        ProxyPassReverse        /css/                           http://svn.internal.pri/css/
        ProxyPass               /images/                        http://svn.internal.pri/images/
        ProxyPassReverse        /images/                        http://svn.internal.pri/images/
        ProxyPass               /svnroot/projects/PCKB/         http://svn.internal.pri/svnroot/projects/PCKB/
        ProxyPassReverse        /svnroot/projects/PCKB/         http://svn.internal.pri/svnroot/projects/PCKB/
        ProxyPass               /svnroot/!svn/                  http://svn.internal.pri/svnroot/!svn/
        ProxyPassReverse        /svnroot/!svn/                  http://svn.internal.pri/svnroot/!svn/

        RewriteEngine   on

        RewriteCond             %{SERVER_NAME}  ^project-svn\.example\.net$ [NC]
        RewriteCond             %{REQUEST_URI}  ^(/svnroot(/projects)?)?/?$ [NC]
        RewriteRule             ^(.*)$          /svnroot/projects/PCKB/ [R=301,L]

        RewriteCond             %{SERVER_NAME}  ^project-svn\.example\.net$ [NC]
        RewriteCond             %{REQUEST_URI}  ^/svnroot/projects/PCKB$ [NC]
        RewriteRule             ^(.*)$          /svnroot/projects/PCKB/ [R=301,L]

        RewriteCond             %{SERVER_NAME}  ^project-svn\.example\.net$ [NC]
        RewriteCond             %{REQUEST_URI}  !^/(svnindex.xsl$|css/.+|images/.+|svnroot/(!svn/|projects/PCKB)).*$ [NC]
        RewriteRule             ^(.*)$          /svnroot/projects/PCKB$0 [R=301]
</VirtualHost>

Few notes about the the configuration:

  • the project we are exporting is PCKB
  • our SVN lives on http://svn.internal.pri/svnroot
  • browser output is beautified through a stylesheet, therefore we’ll need to proxy it also along with all required resources
  • no security is enforced on the proxy:: back end apache handles all security and is configured for complete authentication (read + write) when requesting the repository through the proxy

Operation does not have a SOAP fault extension

boky February 2nd, 2009

If you have ever banged your head when using JAX-WS / WSIT at this error:


[ERROR] fault "getBusinessCaseFault" in operation "getBusinessCase" does not have a SOAP fault extension
line 3213 of file:/service.wsdl

Boy than I have a solution for you. As it turns out, your WSDL was probably generated by Axis. And, as it turns out, it was probably faulty. Our WSDL included 40-something methods and the first 32 were OK, but the last few were invalid and caused the parser to choke.

Look for <wsdl:operation…> tag and in it you’ll see something like this:

<wsdl:fault name="getBusinessCaseHistoryFault">

When in fact, it should be like this:

<wsdl:fault name="getBusinessCaseHistoryFault">
<soap:fault name="getBusinessCaseHistoryFault" use="literal" />
</wsdl:fault>

After we fixed these elements everything went on just fine. I am still not quite sure whether it’s JAX-WS’ or Axis’ bug, the specification seems to be a bit fuzzy on this issue. But nevertheless, things started working.

Just thought it would be nice to share.

Comparing Python and Java

boky September 7th, 2008

Recently I’ve taken up the task of learning Python after being quite a few years in Java. There are some posts — biased towards Python IMHO — that I’ve tried to take into account, but I just thought I’d throw in my €0.02.

Please note that while I’ve been working with Java for quite a few years, I’ve just started learning Python, but it’s not the first dynamic language I’ve used. If anything, I’ve mastered javascript and even used it as a core feature for an insurance enterprise system and are quite aware of pros and cons of dynamic vs. static languages.

If you see anything wrong or disagree, please let me know. Now, let’s go sink our teeth into side by side comparison.

Java vs. Python Productivity - an Overview

Feature Java Python
typing statically typed

Everything (every variable) you use in Java must be explicitly defined. While this presents a bit more clutter and work it eliminates a lot of errors when you type “workunits” or “wokrUnits” instead of “workUnits”. Everything is cought at compile-time (or even before, in your editor!) and there are virtually no mistakes of this kind in production code. Also, with auto boxing and generics, difference between objects and primitive types is blurred to a point where both can be used almost interchangibly.

dinamically typed

In python nothing needs to be defined before you use it. This is quite handy and spares quite a few (unneccessary) lines in your code but if you mistype (or even worse, unknowingly override a global variable), strange things will happen at run-time.

Vedict:Given the fact that there are a lot of languages on both sides of the river and it doesn’t seem that any will be going away soon, there is no clear winner here. Depends on experience of the programmer and the size of the project.
verbosity verbose / not compact

Java does not spare the characters. Everything must be defined and explained long and verbose. This sometimes goes into extreemes (think how much code you need to create a property on an object). On the other hand, IDEs help out with quite a lot of this typing.

terse / compact

Python’s syntax is almost scaringly concise. When you see it first, it might scare you even more than perl.

Vedict: I like to see as much usable code on screen at one time. Unnecessary characters just create clutter. Python gets a point.
classes In Java, everything is focused around a Class, which is why it is generally in it’s own file. But, on the other hand, can contain an unlimited number of inner classes and anonymous classes. A project of 15 classes can therefore be coded in one or 15 files. Phyton focuses its development around modules. Class is defined in much the same way as a function/method. A project of 15 classes can therefore exist in one, 4, 6 or 15 files.
Vedict: IMHO both quite well in this aspect. I never noticed having “too much” files in a Java project. It’s a tie.
exception handling checked and unchecked exceptions

Your method may throw a checked (i.e. explicitly declared) or an unchecked (subclass of RuntimeException) exception. There’s a lot of buzz in the Java world if checked exceptions were a good idea in the first place, but it’s not what our discussion is about.

unchecked only

All Pyhton’s exceptions are unchecked. Period. Any part of the code you do not know might throw some kind of exception you have no idea of.

Vedict: Whether or not checked exceptions are a good or not, I think having a choice is cool. Java wins.
method declaration overloading, varargs

In Java, if you want your method to accept different (…number of…) parameters your only choice was to overload it and include just one line that called the other method. This has been loosened up a bit with varargs (some restrictions apply) which allows you to give unlimited number of parameters to the method.
Example of bogus printf in Java:

String printf(Object... args) {
	printf("Default message", args);
}
 
String printf(String message, Object... args) {
	// code here
}
 
...
 
// Usage
printf(1, 2, 3, 4);
printf("Hello world", "foo","bar", 5.5d);
List a = new ArrayList();
a.add(1);
a.add("foo");
a.add("bar");
printf("Hello world", a);
printf(a);
default params, *name, **name, named parameters

Named parameters are an excellent feature, especially for methods with a lot of parameters. A function/method in python can also accept an optional unbounded list, map or both of parameters in a special variable(s).
Example of bogus printf in Java:

def printf(message="Hello world", *args):
	# the code
 
...
 
# Usage
printf(1, 2, 3, 4)
printf("Hello world", "foo", "bar", 5.5)
# Note:
# printf(message = 'Hello world', "foo", "bar")
# will not work
printf(message = 'Hello world')
Vedict: Named parameters are cool. They increase verbosity (not quite Python’s concept) but for a good reason. Variable argumenst are a bit aquard in Python and easier in java. Python wins, but by a slight margin.
IO handling Java has abstracted everything. Everything is an InputStream/OutputStream, you wrap it around and around to get something useful out of it. Python’s approach is KISS. file.open, urllib2.urlopen.
Vedict: Python is cool. Java is extensible. I must say I miss this kind of simlicitly in Java, but enterprise-wise it makes sense. A tie for now.
inheritance single-class inheritance, interfaces

Java is pretty classic when it comes to object. Each object can inherit from one parent only but can implent multiple interfaces.

multi-class inheritance

One class can inherit from multiple classes. No interfaces.

Vedict: Personally, I find Java’s idea much easier to comprehend and follow. In Python superclass’ constructor may be invoked multiple times. Java wins, sorry Python.
XML parsing DOM built-in, SAX built-in, StAX, jDOM, DOM4J, XOM, JAXB

Comes with unfriendly, but standards-compliant DOM and SAX parsers. Latest java supports DOM 3. Many implementations exist that bring XML parsing closer to Java-mindset.

DOM built-in, SAX built-in, ElementTree, os-enabled (i.e. Expat)…

Comes with unfriendly, but standards-compliant DOM and SAX parsers. Latest Python 2.5 doesn’t seem to support DOM 32. Many other implementations exist, I’ve looked into Python-centric ElementTree.

Vedict: Both support familiar standards and implementations which bring XML loser to the language. Both need a separate downloads to make XML parsing a joy. It’s a tie, folks.
date / time handling The java.util.Date, java.util.Calendar and java.text.DateFormat may not be the gems of best programming ever, but they do get (most) of the job done. Truth to be told, there are some libraries (Joda, being one of them) that try to rectify this situation. There’s even now a new JSR-310 to finally fix everything. Lots of functions in datetime, time and calendar modules with confusing names3 like strftime, strptime, mktime, pryear. Still problem storing and parsing timezones though4.
Vedict: I find Java’s API much easier to understand, although Python’s has more functionality. Given that Java’s API works internationally out-of-the-box and the names are designed for humans to read, I give a point to Java.
unicode Java has been unicode from day 1. Every character, every String is unicode. Period. Java 5.0 Supports Unicode 4.0. Unicode has been “slapped onto” the language. You have “regular strings” (“Hello world”), “raw strings” (r”Hello world”), “unicode strings” (u”Hello world”) and even “unicode raw strings” (ur”Hello world”). It may be true that most of the time unicode strings are not needed and you can save some memory but with current capacities this should not be an issue at all. Just drop the lagacy and make everything unicode. Given Python’s interpreted nature this should not be to difficult IMHO.

Update: Pyhton3 finally fixes this long standing issue.

Vedict: Java wins by far. No question here. With Python3, there’s the gap is closing.

If you are new to Python, there is a nice post explaining its core features in 10 minutes. My verdict so far? Python1 can be more productive in small-scale scenarios. Great language to write scripts, jobs, crons, small, fast and hacked-up webs but not really something I would base my enterprise solution on. But, on the other hand, enterprise applications have the need for scripting, crons, jobs, small tasks which can be edited by the end user (Administrator) without the need for restaring the server or redeploying the application. The best way to accomplish this task would be by using a language such as Python.

Note:

1 I am comparing core language features only, not frameworks built on top of it, i.e. Django, Groovy, Seam or Rails.

2 Tried with node.textContent only.

3 Actually, I haven’t had that much experience with python yet, but as I understand, this are direct wrappers/copies of OS functions with same names.

4 The function strptime, for example, will not parse the timezone in format “+/-hh:mm”. Even named timezones (i.e. PST, DST) are only checked and not processed. You can do it with an external library though.

Hug a developer

Software and media licencing

boky July 2nd, 2008

I was just thinking the other day, how funny the licencing business has become. You “buy” things but you do not really buy them, they’re just “lended” to you for an indefinite period of time. Furthermore you’re allowed to use them only in a certain way within a certain timeframe and should not dissasemble or find out how they work.

Imagine yourself buying a shovel at your local hardware store but the label on it clearly states: “This shovel is licenced for grave-digging only. Any other use, including but not limited to gardening, canal digging or backyard rearaning is strictly forbidden. You are not allowed to dissasemble this shovel, change any of it’s parts — regardless of their condition — or lend it to a friend, coworker, family member or any other person, either living or dead. You are allowed to keep only one (1) copy of shovel at any given time in your shed. If you break any rule stated above, licence for this shovel is revoked and the shovel reclaimed automatically.”

Scary, come to think of it.

Collecting in Java, again

boky June 20th, 2008

Sign at the GoogleplexApache's HTTP server logo

Image via Wikipedia

Seems that a lot of people think that Java Collections framework needs an update. There are some resents to how Apache’s Commons Collections Framework is handling things, most of them focusing on not supporting generics and breaking some contracts of the standard collections API.I’ve talked about Apache’s framework before. But behold: another player has entered the playing fields. This time it’s Google. Funny enough they haven’t tried to fix Apache’s API but rather develop their own API which they hope will get included into JDK 7.

I will do some performance measurements between both and let you know which classes are faster and more optimized. Otherwise, Google presents us with the following (new) collection objects:

  • BiMap, unique two-way map, equivalent to Apache’s BidiMap.
  • Multimap, equivalent to Apache’s MultiMap. Both allow multiple values per single key.
  • Multiset is something that Apache calls a Bag. Both allow multiple instances of the same value to appear in a set.
  • ReferenceMap. Surprise, Apache has a class with the same name.Both do the same. Allow you to use strong/weak/soft references for keys and values. Both are replacements for weirdly designed WeakHashMap.
  • ForwardingMap/List/Set… to aid you in creating your own wrapper classes around other collection objects

The package also adds some utility classes, which can also be found in various other libraries, but it’s nice to know we also have them here:

  • PeekingIterator is something that should be in Java’s collection framework in the first place. It allows you to “peek” at the next element without actually grabbing it, as next() does. While not widely useful, it makes some tasks a lot simpler.
  • Nullable annotation, which allows you to clearly define that null is an acceptable input parameter for a given metthod. Frankly, I think that JetBrains approach is better of, since it provides both Nullable and NotNull, the JAR is only 6KB and annotations are understood by the GUI. Furthermore, results from method cals can also be annotated.
  • numerous other improvements, and utilities to make your life easier, such as creating an ArrayList from Iterator, creating a Map from a list of parameters etc.

Also worth of reading is an interview with Kevin and Jared, authors of the package and especially a comparison that’s available on another blog.

Zemanta Pixie

Next »