You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@shindig.apache.org by et...@apache.org on 2008/12/05 04:01:24 UTC
svn commit: r723563 - in /incubator/shindig/trunk/java:
common/src/main/java/org/apache/shindig/common/cache/
gadgets/src/main/java/org/apache/shindig/gadgets/
gadgets/src/main/java/org/apache/shindig/gadgets/render/
gadgets/src/test/java/org/apache/sh...
Author: etnu
Date: Thu Dec 4 19:01:23 2008
New Revision: 723563
URL: http://svn.apache.org/viewvc?rev=723563&view=rev
Log:
Two changes to substantially reduce garbage generation:
1. Fixed a bug in the serialization of message bundles to JSON. The serialization here was effectively never cached in spite of Louis Ryan's prevous patch to do so (his patch actually made the problem worse without realizing it), which turned out to be producing around 30% of garbage in the entire system. As has been noted before, the org.json library really doesn't scale well and we should probably replace it entirely. The biggest problem is with serialization efficiency.
2. Pre-allocated the buffer used for injecting javascript features into the rendered output. This would likely have no impact on containers that always force the full set of libraries, but on other containers the savings could be substantial.
Note that this removes the no longer necessary AbstractMessageBundle. Now that custom cache implementations can be provided, it's not a needed class. Custom (non HttpFetcher) based lookup can be achieved by overriding fetch* methods.
Removed:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/AbstractMessageBundleFactory.java
Modified:
incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/cache/SoftExpiringCache.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultMessageBundleFactory.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultMessageBundleFactoryTest.java
Modified: incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/cache/SoftExpiringCache.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/cache/SoftExpiringCache.java?rev=723563&r1=723562&r2=723563&view=diff
==============================================================================
--- incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/cache/SoftExpiringCache.java (original)
+++ incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/cache/SoftExpiringCache.java Thu Dec 4 19:01:23 2008
@@ -93,7 +93,7 @@
* Set a new time source. Used for testing, so package-private.
* @param timeSource New time source to use.
*/
- void setTimeSource(TimeSource timeSource) {
+ public void setTimeSource(TimeSource timeSource) {
this.timeSource = timeSource;
}
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultMessageBundleFactory.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultMessageBundleFactory.java?rev=723563&r1=723562&r2=723563&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultMessageBundleFactory.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultMessageBundleFactory.java Thu Dec 4 19:01:23 2008
@@ -21,10 +21,12 @@
import org.apache.shindig.common.cache.Cache;
import org.apache.shindig.common.cache.CacheProvider;
import org.apache.shindig.common.cache.SoftExpiringCache;
+import org.apache.shindig.common.cache.SoftExpiringCache.CachedObject;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.gadgets.http.HttpFetcher;
import org.apache.shindig.gadgets.http.HttpRequest;
import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
import org.apache.shindig.gadgets.spec.LocaleSpec;
import org.apache.shindig.gadgets.spec.MessageBundle;
@@ -32,17 +34,22 @@
import com.google.inject.Singleton;
import com.google.inject.name.Named;
+import java.util.Locale;
import java.util.logging.Logger;
/**
* Default implementation of a message bundle factory.
+ *
+ * Containers wishing to implement custom bundle fetching behavior should override
+ * {@link #fetchBundle}.
*/
@Singleton
-public class DefaultMessageBundleFactory extends AbstractMessageBundleFactory {
+public class DefaultMessageBundleFactory implements MessageBundleFactory {
+ private static final Locale ALL_ALL = new Locale("all", "ALL");
public static final String CACHE_NAME = "messageBundles";
static final Logger LOG = Logger.getLogger(DefaultMessageBundleFactory.class.getName());
private final HttpFetcher fetcher;
- private final SoftExpiringCache<Uri, MessageBundle> cache;
+ final SoftExpiringCache<String, MessageBundle> cache;
private final long refresh;
@Inject
@@ -50,37 +57,37 @@
CacheProvider cacheProvider,
@Named("shindig.cache.xml.refreshInterval") long refresh) {
this.fetcher = fetcher;
- Cache<Uri, MessageBundle> baseCache = cacheProvider.createCache(CACHE_NAME);
- this.cache = new SoftExpiringCache<Uri, MessageBundle>(baseCache);
+ Cache<String, MessageBundle> baseCache = cacheProvider.createCache(CACHE_NAME);
+ this.cache = new SoftExpiringCache<String, MessageBundle>(baseCache);
this.refresh = refresh;
}
- @Override
- protected MessageBundle fetchBundle(LocaleSpec locale, boolean ignoreCache)
+ public MessageBundle getBundle(GadgetSpec spec, Locale locale, boolean ignoreCache)
throws GadgetException {
+
if (ignoreCache) {
- return fetchAndCacheBundle(locale, ignoreCache);
+ return getNestedBundle(spec, locale, true);
}
- Uri uri = locale.getMessages();
-
- SoftExpiringCache.CachedObject<MessageBundle> cached = cache.getElement(uri);
+ String key = spec.getUrl().toString() + '.' + locale.toString();
+ CachedObject<MessageBundle> cached = cache.getElement(key);
- MessageBundle bundle = null;
+ MessageBundle bundle;
if (cached == null || cached.isExpired) {
try {
- bundle = fetchAndCacheBundle(locale, ignoreCache);
+ bundle = getNestedBundle(spec, locale, ignoreCache);
} catch (GadgetException e) {
// Enforce negative caching.
if (cached != null) {
+ LOG.info("MessageBundle fetch failed for " + key + " - using cached.");
bundle = cached.obj;
} else {
// We create this dummy spec to avoid the cost of re-parsing when a remote site is out.
+ LOG.info("MessageBundle fetch failed for " + key + " - using default.");
bundle = MessageBundle.EMPTY;
}
- LOG.info("MessageBundle fetch failed for " + uri + " - using cached.");
- cache.addElement(uri, bundle, refresh);
}
+ cache.addElement(key, bundle, refresh);
} else {
bundle = cached.obj;
}
@@ -88,7 +95,38 @@
return bundle;
}
- private MessageBundle fetchAndCacheBundle(LocaleSpec locale, boolean ignoreCache)
+ private MessageBundle getNestedBundle(GadgetSpec spec, Locale locale, boolean ignoreCache)
+ throws GadgetException {
+ MessageBundle parent = getParentBundle(spec, locale, ignoreCache);
+ MessageBundle child = null;
+ LocaleSpec localeSpec = spec.getModulePrefs().getLocale(locale);
+ if (localeSpec == null) {
+ return parent == null ? MessageBundle.EMPTY : parent;
+ }
+ Uri messages = localeSpec.getMessages();
+ if (messages == null || messages.toString().length() == 0) {
+ child = localeSpec.getMessageBundle();
+ } else {
+ child = fetchBundle(localeSpec, ignoreCache);
+ }
+ return new MessageBundle(parent, child);
+ }
+
+ private MessageBundle getParentBundle(GadgetSpec spec, Locale locale, boolean ignoreCache)
+ throws GadgetException {
+ if (locale.getLanguage().equalsIgnoreCase("all")) {
+ // Top most locale already.
+ return null;
+ }
+
+ if (locale.getCountry().equalsIgnoreCase("ALL")) {
+ return getBundle(spec, ALL_ALL, ignoreCache);
+ }
+
+ return getBundle(spec, new Locale(locale.getLanguage(), "ALL"), ignoreCache);
+ }
+
+ protected MessageBundle fetchBundle(LocaleSpec locale, boolean ignoreCache)
throws GadgetException {
Uri url = locale.getMessages();
HttpRequest request = new HttpRequest(url).setIgnoreCache(ignoreCache);
@@ -104,7 +142,6 @@
}
MessageBundle bundle = new MessageBundle(locale, response.getResponseAsString());
- cache.addElement(url, bundle, refresh);
return bundle;
}
}
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java?rev=723563&r1=723562&r2=723563&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java Thu Dec 4 19:01:23 2008
@@ -234,13 +234,24 @@
}
}
- StringBuilder inlineJs = new StringBuilder();
-
// Inline any libs that weren't forced. The ugly context switch between inline and external
// Js is needed to allow both inline and external scripts declared in feature.xml.
String container = context.getContainer();
Collection<GadgetFeature> features = getFeatures(spec, forced);
+ // Precalculate the maximum length in order to avoid excessive garbage generation.
+ int size = 0;
+ for (GadgetFeature feature : features) {
+ for (JsLibrary library : feature.getJsLibraries(RenderingContext.GADGET, container)) {
+ if (library.getType().equals(JsLibrary.Type.URL)) {
+ size += library.getContent().length();
+ }
+ }
+ }
+
+ // Really inexact.
+ StringBuilder inlineJs = new StringBuilder(size);
+
for (GadgetFeature feature : features) {
for (JsLibrary library : feature.getJsLibraries(RenderingContext.GADGET, container)) {
if (library.getType().equals(JsLibrary.Type.URL)) {
Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultMessageBundleFactoryTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultMessageBundleFactoryTest.java?rev=723563&r1=723562&r2=723563&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultMessageBundleFactoryTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultMessageBundleFactoryTest.java Thu Dec 4 19:01:23 2008
@@ -23,10 +23,13 @@
import static org.easymock.EasyMock.verify;
import static org.easymock.classextension.EasyMock.replay;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import org.apache.shindig.common.cache.Cache;
import org.apache.shindig.common.cache.CacheProvider;
import org.apache.shindig.common.cache.LruCacheProvider;
import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.TimeSource;
import org.apache.shindig.gadgets.http.HttpFetcher;
import org.apache.shindig.gadgets.http.HttpRequest;
import org.apache.shindig.gadgets.http.HttpResponse;
@@ -38,6 +41,7 @@
import org.junit.Test;
import java.util.Locale;
+import java.util.concurrent.atomic.AtomicLong;
/**
* Tests for DefaultMessageBundleFactory
@@ -80,11 +84,13 @@
"<Content type='html'/>" +
"</Module>";
- private static final int MAX_AGE = -10000;
+ private static final int MAX_AGE = 10000;
private final HttpFetcher fetcher = EasyMock.createNiceMock(HttpFetcher.class);
private final CacheProvider cacheProvider = new LruCacheProvider(10);
- private final MessageBundleFactory bundleFactory
+ private final Cache<String, MessageBundle> cache
+ = cacheProvider.createCache(DefaultMessageBundleFactory.CACHE_NAME);
+ private final DefaultMessageBundleFactory bundleFactory
= new DefaultMessageBundleFactory(fetcher, cacheProvider, MAX_AGE);
private final GadgetSpec gadgetSpec;
@@ -111,6 +117,18 @@
}
@Test
+ public void getBundleFromCache() throws Exception {
+ HttpResponse response = new HttpResponse(BASIC_BUNDLE);
+ expect(fetcher.fetch(isA(HttpRequest.class))).andReturn(response).once();
+ replay(fetcher);
+
+ MessageBundle bundle0 = bundleFactory.getBundle(gadgetSpec, LOCALE, false);
+ MessageBundle bundle1 = bundleFactory.getBundle(gadgetSpec, LOCALE, false);
+
+ assertSame("Different objects returned out of the cache", bundle0, bundle1);
+ }
+
+ @Test
public void getParentBundle() throws Exception {
MessageBundle bundle = bundleFactory.getBundle(gadgetSpec, PARENT_LOCALE, true);
@@ -126,6 +144,12 @@
}
@Test
+ public void ignoreCacheDoesNotStore() throws Exception {
+ bundleFactory.getBundle(gadgetSpec, new Locale("all", "ALL"), true);
+ assertEquals(0, cache.getSize());
+ }
+
+ @Test
public void badResponseServedFromCache() throws Exception {
HttpResponse expiredResponse = new HttpResponseBuilder()
.setResponse(BASIC_BUNDLE.getBytes("UTF-8"))
@@ -139,24 +163,53 @@
.andReturn(badResponse).once();
replay(fetcher);
- MessageBundle bundle = bundleFactory.getBundle(gadgetSpec, LOCALE, true);
- bundle = bundleFactory.getBundle(gadgetSpec, LOCALE, false);
+ final AtomicLong time = new AtomicLong();
+
+ bundleFactory.cache.setTimeSource(new TimeSource() {
+ @Override
+ public long currentTimeMillis() {
+ return time.get();
+ }
+ });
+
+ time.set(System.currentTimeMillis());
+
+ MessageBundle bundle0 = bundleFactory.getBundle(gadgetSpec, LOCALE, false);
+
+ time.set(time.get() + MAX_AGE + 1);
+
+ MessageBundle bundle1 = bundleFactory.getBundle(gadgetSpec, LOCALE, false);
verify(fetcher);
- assertEquals(MSG_0_VALUE, bundle.getMessages().get(MSG_0_NAME));
+ assertSame("Did not respond from cache when refresh failed.", bundle0, bundle1);
+ }
+
+ @Test
+ public void badResponseIsEmptyWhenNotInCache() throws Exception {
+ HttpResponse badResponse = HttpResponse.error();
+
+ expect(fetcher.fetch(isA(HttpRequest.class)))
+ .andReturn(badResponse).once();
+ replay(fetcher);
+
+ MessageBundle bundle = bundleFactory.getBundle(gadgetSpec, LOCALE, false);
+
+ verify(fetcher);
+
+ assertEquals(0, bundle.getMessages().size());
}
@Test
public void ttlPropagatesToFetcher() throws Exception {
CapturingFetcher capturingFetcher = new CapturingFetcher();
- MessageBundleFactory forcedTtlFactory
- = new DefaultMessageBundleFactory(capturingFetcher, cacheProvider, 10000);
+ MessageBundleFactory factory
+ = new DefaultMessageBundleFactory(capturingFetcher, cacheProvider, MAX_AGE);
- forcedTtlFactory.getBundle(gadgetSpec, LOCALE, false);
+ factory.getBundle(gadgetSpec, LOCALE, false);
- assertEquals(10, capturingFetcher.request.getCacheTtl());
+ assertEquals(MAX_AGE / 1000, capturingFetcher.request.getCacheTtl());
}
private static class CapturingFetcher implements HttpFetcher {
Re: svn commit: r723563 - in /incubator/shindig/trunk/java:
Posted by Kevin Brown <et...@google.com>.
On Thu, Dec 4, 2008 at 8:26 PM, Henning P. Schmiedehausen <
henning@schmiedehausen.org> wrote:
> etnu@apache.org writes:
>
> >Author: etnu
> >Date: Thu Dec 4 19:01:23 2008
> >New Revision: 723563
>
> >URL: http://svn.apache.org/viewvc?rev=723563&view=rev
> >Log:
> >Two changes to substantially reduce garbage generation:
>
> Why are these changes not two separate commits? Are they related? Other
> than that they reduce "garbage generation"?
These two bits of code represent between 50 and 90% of the allocations in a
production deployment, which in turn causes substantially more gc activity
yielding lots of wasted resources.
They could have been separate changes, sure.
>
> The javadoc directly contradicts the change.
Indeed.
>
>
> >-public class DefaultMessageBundleFactory extends
> AbstractMessageBundleFactory {
> >+public class DefaultMessageBundleFactory implements MessageBundleFactory
> {
>
> Is the AbstractMessageBundleFactory still used? If not, can it go?
It is gone, as mentioned in the commit message.
> >+ private static final Locale ALL_ALL = new Locale("all", "ALL");
> > public static final String CACHE_NAME = "messageBundles";
> > static final Logger LOG =
> Logger.getLogger(DefaultMessageBundleFactory.class.getName());
> > private final HttpFetcher fetcher;
> >- private final SoftExpiringCache<Uri, MessageBundle> cache;
> >+ final SoftExpiringCache<String, MessageBundle> cache;
>
> Use a getter. The usage of direct field access in the tests will lead to
> brittleness. (see DefaultMessageBundleFactoryTest)
It'd be a package private getter in any case (we only care about reading it
in the test), so I don't really think it matters much one way or another.
>
>
> > private final long refresh;
> >
> > @Inject
> >@@ -50,37 +57,37 @@
>
> >Modified:
> incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java
> >URL:
> http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java?rev=723563&r1=723562&r2=723563&view=diff
>
> >==============================================================================
> >---
> incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java
> (original)
> >+++
> incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java
> Thu Dec 4 19:01:23 2008
> >@@ -234,13 +234,24 @@
> > }
> > }
> >
> >- StringBuilder inlineJs = new StringBuilder();
> >-
> > // Inline any libs that weren't forced. The ugly context switch
> between inline and external
> > // Js is needed to allow both inline and external scripts declared in
> feature.xml.
> > String container = context.getContainer();
> > Collection<GadgetFeature> features = getFeatures(spec, forced);
> >
> >+ // Precalculate the maximum length in order to avoid excessive
> garbage generation.
> >+ int size = 0;
> >+ for (GadgetFeature feature : features) {
> >+ for (JsLibrary library :
> feature.getJsLibraries(RenderingContext.GADGET, container)) {
> >+ if (library.getType().equals(JsLibrary.Type.URL)) {
> >+ size += library.getContent().length();
> >+ }
> >+ }
> >+ }
> >+
> >+ // Really inexact.
> >+ StringBuilder inlineJs = new StringBuilder(size);
> >+
>
> How much garbage are we talking about? Is that significant (and how often
> is that code run)?
Every single gadget render, about 10% of allocations.
>
>
> >
> >- private static final int MAX_AGE = -10000;
> >+ private static final int MAX_AGE = 10000;
>
> Why this change?
To make the caching logic tests work. Previously the value didn't actually
matter, now it does.
Re: svn commit: r723563 - in /incubator/shindig/trunk/java:
Posted by "Henning P. Schmiedehausen" <he...@schmiedehausen.org>.
etnu@apache.org writes:
>Author: etnu
>Date: Thu Dec 4 19:01:23 2008
>New Revision: 723563
>URL: http://svn.apache.org/viewvc?rev=723563&view=rev
>Log:
>Two changes to substantially reduce garbage generation:
Why are these changes not two separate commits? Are they related? Other than that they reduce "garbage generation"?
>1. Fixed a bug in the serialization of message bundles to JSON. The serialization here was effectively never cached in spite of Louis Ryan's prevous patch to do so (his patch actually made the problem worse without realizing it), which turned out to be producing around 30% of garbage in the entire system. As has been noted before, the org.json library really doesn't scale well and we should probably replace it entirely. The biggest problem is with serialization efficiency.
>--- incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/cache/SoftExpiringCache.java (original)
>+++ incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/cache/SoftExpiringCache.java Thu Dec 4 19:01:23 2008
>@@ -93,7 +93,7 @@
> * Set a new time source. Used for testing, so package-private.
> * @param timeSource New time source to use.
> */
>- void setTimeSource(TimeSource timeSource) {
>+ public void setTimeSource(TimeSource timeSource) {
> this.timeSource = timeSource;
> }
The javadoc directly contradicts the change.
>-public class DefaultMessageBundleFactory extends AbstractMessageBundleFactory {
>+public class DefaultMessageBundleFactory implements MessageBundleFactory {
Is the AbstractMessageBundleFactory still used? If not, can it go?
>+ private static final Locale ALL_ALL = new Locale("all", "ALL");
> public static final String CACHE_NAME = "messageBundles";
> static final Logger LOG = Logger.getLogger(DefaultMessageBundleFactory.class.getName());
> private final HttpFetcher fetcher;
>- private final SoftExpiringCache<Uri, MessageBundle> cache;
>+ final SoftExpiringCache<String, MessageBundle> cache;
Use a getter. The usage of direct field access in the tests will lead to brittleness. (see DefaultMessageBundleFactoryTest)
> private final long refresh;
>
> @Inject
>@@ -50,37 +57,37 @@
>Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java
>URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java?rev=723563&r1=723562&r2=723563&view=diff
>==============================================================================
>--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java (original)
>+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java Thu Dec 4 19:01:23 2008
>@@ -234,13 +234,24 @@
> }
> }
>
>- StringBuilder inlineJs = new StringBuilder();
>-
> // Inline any libs that weren't forced. The ugly context switch between inline and external
> // Js is needed to allow both inline and external scripts declared in feature.xml.
> String container = context.getContainer();
> Collection<GadgetFeature> features = getFeatures(spec, forced);
>
>+ // Precalculate the maximum length in order to avoid excessive garbage generation.
>+ int size = 0;
>+ for (GadgetFeature feature : features) {
>+ for (JsLibrary library : feature.getJsLibraries(RenderingContext.GADGET, container)) {
>+ if (library.getType().equals(JsLibrary.Type.URL)) {
>+ size += library.getContent().length();
>+ }
>+ }
>+ }
>+
>+ // Really inexact.
>+ StringBuilder inlineJs = new StringBuilder(size);
>+
How much garbage are we talking about? Is that significant (and how often is that code run)?
>
>- private static final int MAX_AGE = -10000;
>+ private static final int MAX_AGE = 10000;
Why this change?
Ciao
Henning
--
Henning P. Schmiedehausen - Palo Alto, California, U.S.A.
henning@schmiedehausen.org "We're Germans and we use Unix.
henning@apache.org That's a combination of two demographic groups
known to have no sense of humour whatsoever."
-- Hanno Mueller, de.comp.os.unix.programming