 |
 |

March 1999

FUELING INNOVATION
More search options...
Search jCentral
Search IDG.net
Topical index
Net News Central
Developer Tools Guide
Book Catalog
Writers Guidelines

Advertisement
JavaWorld Partners
CareerCentral
Bookstore
Software store
Sponsored by JW
JavaWorld Programs
|
 |
 |
 |
The Volano Report: Which Java platform is fastest, most scalable? A JavaWorld exclusive!
Results of new VolanoMark 2.1 server benchmarks reveal how 10 virtual machines stack up on 6 OSs
Summary
Microsoft said it would never happen -- that Sun would never
surpass it in Java performance. And in fact, according to these new
benchmarks, you could argue that it still hasn't happened. Yet while Sun
and Microsoft battled it out in public, another Java vendor quietly, very
quietly, stole the lead, beating Sun with its own technology and
Microsoft on its own operating system. At the same time, Sun made
enormous gains in Java scalability on its Solaris platform.
In this follow-up to the server-side
Java performance and scalability feature published in the August
1998 issue of JavaWorld, author John Neffenger, founder and CTO
of Volano LLC and creator of the VolanoMark benchmark, delivers the
latest results of VolanoMark 2.1 tests on ten Intel-based Java platforms --
and in the process answers the all-important question: Which operating
system and Java virtual machine will provide the best Java server platform
for your Intel box? PLUS: Learn how IBM dramatically
improved scalability on Windows NT, and a simple step you can take to
quadruple the number of threads you can handle on
Windows!
Tune into this JavaWorld exclusive for a look at who's leading
the pack today and how they did it. (4,000 words)
By John Neffenger
peed and scalability are the crucial elements for the successful
deployment of a Java server platform, as any Java software vendor
can attest. But most administrators possess neither the resources nor
the time to test the various server-side Java platforms on the market.
JavaWorld therefore turned to Volano and the VolanoMark 2.1 benchmark
for answers.
The tests presented in this report look at the performance and network
scalability of ten Java virtual machines on six operating systems using
a common Intel hardware platform running VolanoMark 2.1. VolanoMark
attempts to answer two questions about Java virtual machines used in
network server environments -- especially those environments involving a
large number of active socket connections:
- Is it fast?
- Does it scale?
VolanoMark isn't a Java virtual machine test. It's a Java platform test.
It allows you to take one set of Java class files and use them to
evaluate an entire Java system, including the Java virtual machine, the
operating system, the hardware, and the network. Each of the tests in
this article was executed on identical Intel hardware, providing an
answer to the question our customers ask most -- Which operating system
and Java virtual machine will provide the best Java server platform for my
Intel box?
Unlike Java support for applets on the client side (where two virtual
machines -- those provided by Microsoft and Netscape in their respective
Web browsers -- dominate), the leaders in Java on the server side change
monthly. Nowhere is that more apparent than for Intel-based systems,
which provide dozens of Java VM alternatives. Volano customers can and
do switch overnight from one Java virtual machine to another -- or even
from one operating system to another -- in order to gain the best
performance or network scalability from their Java applications.
What is VolanoMark?
VolanoMark is a 100% Pure Java server benchmark characterized by
long-lasting network connections and high thread counts. In this
context, long-lasting means the connections last several
minutes or longer, rather than just a few seconds. The VolanoMark
benchmark creates client connections in groups of 20 and measures how
long it takes for the clients to take turns broadcasting their messages
to the group. At the end of the test, it reports a score as the average
number of messages transferred by the server per second. Its results
have accurately predicted the real-world Java performance and
scalability of our VolanoChat product line for more than two years.
Furthermore, VolanoMark seems to hit Java platforms where it hurts the
most, not only creating a large number of active socket connections and
threads but forcing the system to constantly switch among them all. The
resulting scores are dominated by several factors:
- Whether the Java virtual machine uses a just-in-time compiler (JIT)
- Whether the Java virtual machine uses native threads
- How many threads per process are supported by the operating system
- How many network connections per process are supported by the operating system
- How the Java virtual machine maps Java threads onto operating-system threads
VolanoMark 2.1 is available as a free download from Volano's Web site.
(See the Resources section below.)
Is it fast?
Each time I run a set of VolanoMark tests against the latest batch of
Java virtual machines I find some surprises, and this time is no
exception. IBM now dominates VolanoMark performance, whether on Windows
NT 4.0 or on its new OS/2 Warp Server for e-business. IBM's Java virtual
machine for Windows NT is a remarkable 20 percent faster than the
alternatives from Microsoft and Sun. On OS/2 Warp Server it even beats
Tower Technology's TowerJ static compiler, showing that it is possible
for normal Java virtual machines to surpass native performance, even if
that's not yet happening on the same operating system.
Each time I run
a set of VolanoMark
tests I find surprises,
and this time is
no exception.
|
The performance of Tower Technology's latest TowerJ 2.2.7 is still
superb, as should be expected from a static compiler. TowerJ takes Java
class files and converts them into C source code, which it then compiles
into a native executable program. TowerJ 3.0, due for release in March,
can dynamically load Java class files in both their original form and in
their native TowerJ-compiled form. But unlike all the other Java virtual
machines shown here, TowerJ isn't free. (See Resources for the pricing structure.)
Microsoft's and Sun's Java VMs on Windows both fall right in the middle
of the pack, with Novell's NetWare Java VM right behind them. Sun's JDK
1.2 [aka the Java 2 platform --editor] on Solaris executes at about half the speed
of the top performers, but once you discover its network scalability scores
below, you won't care!
The Blackdown Java-Linux port now has a good just-in-time compiler
called TYA, which gives it roughly a 50 percent performance improvement over
its score without TYA. The latest release of the FreeBSD Java VM is
very stable but suffers in performance from its lack of a just-in-time
compiler.
Transvirtual's Kaffe OpenVM holds a lot of promise and even convinced a
panel of 21 judges that it's the Best
Virtual Machine. Unfortunately, that's not a conclusion I can share
based on my own tests. For more than a year, I have been unable to run
VolanoMark with Kaffe on several different operating systems. I'm still
hopeful, however, since Kaffe holds the best promise of providing Java support
for some non-Intel Linux systems, such as those from Cobalt Networks.
Run rules
The performance tests were executed with the following commands on a
local loopback connection, using the heap size options shown below where
possible:
- Server:
java -ms8m -mx64m COM.volano.Main
- Client:
java -ms8m -mx64m COM.volano.Mark -count 100
See the COM.volano.Mark
command synopsis for a complete description of all options.
The operating system was rebooted before each set of tests for a
particular Java virtual machine, and the first test result was
discarded. The server side was restarted before each run of the client
benchmark. The final score is the average of the best two out of three
subsequent results.
Results
Scores are the throughput of the server in messages per second. Bigger
numbers are faster. See the "Environments"
section for details on the hardware platform, operating systems, and
Java virtual machine environments.
| Java platform |
Score |
IBM JDK 1.1.7 OS/2
Tower TowerJ 2.2.7 Linux
IBM JDK 1.1.7 Windows
Microsoft SDK 3.1 Windows
Sun JDK 1.2 Windows
Novell JDK 1.1.5 NetWare
Sun JDK 1.2 Solaris
Blackdown JDK 1.1.7 Linux
JDK 1.1.7 FreeBSD
Transvirtual Kaffe 1.0 Linux |
1857
1817
1660
1412
1344
1218
912
448
166
|
Figure 1. VolanoMark 2.1.2 local performance test, measuring throughput in messages per second.
| Java platform |
Operating system |
Results |
Score |
IBM JDK 1.1.7 OS/2 |
OS/2 Warp Server for e-business |
1832, 1867, 1847 |
1857 |
Tower TowerJ 2.2.7 Linux |
Red Hat Linux 5.2 Intel |
1820, 1801, 1814 |
1817 |
IBM JDK 1.1.7 Windows |
Windows NT Workstation 4.0 |
1652, 1664, 1656 |
1660 |
Microsoft SDK 3.1 Windows |
Windows NT Workstation 4.0 |
1413, 1407, 1411 |
1412 |
Sun JDK 1.2 Windows |
Windows NT Workstation 4.0 |
1344, 1342, 1344 |
1344 |
Novell JDK 1.1.5 NetWare |
NetWare 5 |
1216, 1216, 1220a |
1218 |
Sun JDK 1.2 Solaris |
Solaris 7 Desktop Intel Platform Edition |
915, 909, 908 |
912 |
Blackdown JDK 1.1.7 Linux |
Red Hat Linux 5.2 Intel |
447, 447, 449 |
448 |
JDK 1.1.7 FreeBSD |
FreeBSD 2.2.8-RELEASE |
166, 166, 165 |
166 |
Transvirtual Kaffe 1.0 Linux |
Red Hat Linux 5.2 Intel |
SocketExceptionb |
--- |
Table 1. VolanoMark 2.1.2 local performance test,
measuring throughput in messages per second. The final score is the
average of the best two out of three results. All tests ran identical copies
of VolanoMark 2.1.2 on identical hardware.
Notes:
- Received
"java.net.SocketException: Bad file number" 36
times on the third run, but the test completed successfully.
- Failed with
"java.net.SocketException: Unimplemented socket
option" when the server called Socket.setSoTimeout.
Although the latest Kaffe build supports socket timeouts, it still fails
to run VolanoMark.
Does it scale?
The fundamental problems with Java performance for Volano's products
were solved back in the fall of 1997 with the release of Sun's JDK 1.1.3
on Solaris 2.6 and Microsoft's Internet Explorer 3.0 Java VM for
Windows. Granted, there's no such thing as too fast, but even FreeBSD
and Linux have Java support fast enough for all but the most demanding
Java server applications.
Much more interesting is what happens to all that speed when we increase
the number of simultaneous connections to the Java application. In my
previous VolanoMark article for JavaWorld, I tested a dozen Java
platforms for their performance in handling 100 to 900 concurrent
connections. This time I pushed that number up to 2,100 connections. As
you can see from the table below, there's still plenty of room for
improvement.
Only two out of the ten Java platforms even survived the test, and only
one survived without errors. Sun's JDK 1.2 on Solaris 7, still an early
access release, finally shows it's possible to handle large numbers of
connections to a single Java application without destroying performance
(and without any errors). In fact, Sun's performance at 2,100
connections is only 23 percent below its performance at 300 connections!
Only two out of the
ten Java platforms
even survived the test,
and only one survived
without errors.
|
IBM also provided some surprises, breaking past VolanoMark's
1,000-connection barrier on Windows NT, which has held back Sun and
Microsoft for the past two years. I've been asking IBM how it did that
ever since I first saw the results, and the company agreed to disclose
the not-so-secret secret (see sidebar).
Microsoft and Sun, take notes.
IBM's JDK 1.1.7 on OS/2 Warp Server for e-business died after 600
concurrent connections because of a bug in its uniprocessor kernel that
prevented the Java VM from using high virtual memory. IBM says the
problem will be fixed in the final release of OS/2 Warp Server for
e-business, allowing for at least 1,100 concurrent connections to a
single Java application process.
TowerJ and the Blackdown Java-Linux port were unable to break past the
Linux kernel's default limit of 256 file descriptors per process. For my
previous VolanoMark article, I rebuilt the Linux kernel to allow up to
1,024 file descriptors per process only to discover that neither TowerJ
2.1.2 nor Blackdown JDK 1.1.6 took advantage of the change. The new
Linux version 2.2 kernel should provide some relief in this area.
The FreeBSD Java port, along with the FreeBSD operating system, can
easily handle up to 2,048 file descriptors per process, but its
relatively poor performance makes attempts at anything greater than 600
connections unbearable. A good just-in-time compiler would solve that
problem.
Novell's NetWare 5 Java VM, on the other hand, seems to be limited only
by the amount of real memory on the system, although I was unable to
obtain results above 1,500 concurrent connections with the 256 MB
of RAM on my test system.
Run rules
The network scalability test was executed with the following commands
over an isolated 10-Mbps Ethernet connection with a 10-Mbps hub, using
the heap and stack size options shown below where possible:
- Server:
java -ms8m -mx128m -ss32k COM.volano.Main
- Client:
-
java -ms8m -mx128m -ss32k COM.volano.Mark -host xxx.yyy.zzz -rooms 15
java -ms8m -mx128m -ss32k COM.volano.Mark -host xxx.yyy.zzz -rooms 30
...
java -ms8m -mx128m -ss32k COM.volano.Mark -host xxx.yyy.zzz -rooms 105
See the COM.volano.Mark
command synopsis for a complete description of all options.
The client test driver was executed under the IBM JDK 1.1.7 Java virtual
machine using Windows NT 4.0 on a 200-MHz Intel Pentium Pro with a
256-KB L2 cache and 256 MB of RAM. The operating systems on
both sides were rebooted before each set of tests for a particular Java
virtual machine. The VolanoMark server was not restarted between client
test runs except where noted.
Results
Scores are the throughput of the server in messages per second based on
the total number of concurrent connections. Bigger numbers are faster.
See the "Environments" section for details on
the hardware platform, operating systems, and Java virtual machine
environments.
|
|
|
|
|
|
|
| 300 |
600 |
900 |
1200 |
1500 |
1800 |
2100 |
IBM JDK 1.1.7 OS/2
Tower TowerJ 2.2.7 Linux
IBM JDK 1.1.7 Windows
|
Microsoft SDK 3.1 Windows
Sun JDK 1.2 Windows
Novell JDK 1.1.5 NetWare
|
Sun JDK 1.2 Solaris
Blackdown JDK 1.1.7 Linux
JDK 1.1.7 FreeBSD
|
|
Figure 2. VolanoMark 2.1.2 network scalability test,
measuring throughput in messages per second based on the number of
concurrent connections.
| Java platform |
300 |
600 |
900 |
1200 |
1500 |
1800 |
2100 |
Limit |
IBM JDK 1.1.7 OS/2 |
2192 |
1907 |
----a |
---- |
---- |
---- |
---- |
~ 600 |
Tower TowerJ 2.2.7 Linux |
----b |
---- |
---- |
---- |
---- |
---- |
---- |
247 |
IBM JDK 1.1.7 Windows |
2535 |
2181 |
1753 |
1499 |
1452 |
755c |
498c |
~ 3500 |
Microsoft SDK 3.1 Windows |
2368 |
1943 |
1554 |
----a |
---- |
---- |
---- |
962 |
Sun JDK 1.2 Windows |
2340 |
1831 |
1482 |
----a |
---- |
---- |
---- |
907 |
Novell JDK 1.1.5 NetWare |
2487 |
2014 |
1844 |
1293 |
1505 |
----d |
---- |
~ 1500 |
Sun JDK 1.2 Solaris |
1528 |
1461 |
1384 |
1337 |
1286 |
1241 |
1183 |
~ 4000 |
Blackdown JDK 1.1.7 Linux |
----e |
---- |
---- |
---- |
---- |
---- |
---- |
247 |
JDK 1.1.7 FreeBSD |
237 |
135 |
----f |
---- |
---- |
---- |
---- |
~ 600 |
Table 2. VolanoMark 2.1.2 network scalability test,
measuring throughput in messages per second based on the number of
concurrent connections. All tests ran identical copies of VolanoMark
2.1.2 on identical hardware. The Limit is the maximum number of simultaneous
VolanoMark connections possible (shown as an estimate when preceded by ~).
Notes:
- Failed with
"java.lang.OutOfMemoryError". To work
around the problem, see below.
- Failed with
"java.net.SocketException: accept: Invalid
argument".
- Failed on the first try at 1,800 and 2,100 connections with
"java.net.SocketException: Socket read failed: 10055" and
"java.net.SocketException: Connection shutdown". Restarted
the server and it worked on the second try.
- Took too long to complete at 1,800 connections (thrashing).
- Failed with
"java.net.SocketException: Invalid
argument".
- Failed with client dead session timeouts.
Just enter one command
to quadruple the number
of threads each
Java virtual machine
can handle.
|
The stack reserve
As IBM has demonstrated, both Sun and Microsoft have artificially
crippled the connection scalability of their Java virtual machines on
Windows NT by shipping them with the default stack reserve of one
MB per thread. Microsoft long ago changed the stack reserve of its
own Internet Explorer 4.0 Web browser and Internet Information Server
4.0 Web server, following the "changed standard for all Windows NT
system executables." (See the Resources section
below.) Why it didn't apply the same change to its Java VM, or why
Sun failed to pick up the change for its Windows NT port, is unclear to
me. In any case, it's now time for them to follow IBM's lead and make
this simple, but important, change.
You can make the change yourself if you buy the Microsoft Visual C++
development environment and use the Microsoft Binary File Editor. Just
enter "editbin /stack:262144 java.exe" or "editbin
/stack:262144 jview.exe" on the command line to quadruple the
number of threads each Java virtual machine can handle.
After modifying the size of the stack reserve on the Microsoft and Sun
Java virtual machines on Windows, I obtained the results shown below:
| Java platform |
300 |
600 |
900 |
1200 |
1500 |
1800 |
2100 |
Limit |
Sun JDK 1.2 Windows |
2381 |
1812 |
1486 |
1215 |
928 |
732 |
622 |
~ 3600 |
Microsoft SDK 3.1 Windows |
2409 |
1923 |
1577 |
1200 |
544a |
524 |
459 |
~ 3800 |
Table 3. VolanoMark 2.1.2 network scalability test of
the Sun and Microsoft Java virtual machines for Windows NT after
reducing the stack reserve from 1 MB to 256 KB per thread. Throughput is
reported in messages per second based on the number of concurrent
connections. All tests ran identical copies of VolanoMark 2.1.2 on
identical hardware. The Limit is the maximum number of simultaneous
VolanoMark connections possible (shown as an estimate when preceded by ~).
Notes:
- Failed on the first try at 1,500 connections with
"java.net.SocketException: Connection reset by peer" on
client side. Restarted the server and it worked on the second try.
Environments
All tests ran identical copies of VolanoMark 2.1.2 on identical hardware
-- a 200-MHz Intel Pentium Pro processor with a 256-KB L2 cache
and 256 MB of RAM.
- IBM JDK 1.1.7 OS/2 preview operating system
-
- 20 November 1998 IBM OS/2 Warp Developer Kit, Java Edition, Version 1.1.7
- IBM OS/2 Warp Server for e-business Preview (Internal revision
14.020F_UNI, planned for release in 2nd Quarter 1999)
- java full version
"JDK 1.1.7A IBM build o117-19981120 (JIT enabled: javax)"
- Installed from
javaintk.exe (13,659,042 bytes) and
javainuf.exe (17,947,952 bytes).
- Uses native threads and
javax just-in-time compiler.
- Modified the
CONFIG.SYS file to increase the thread
limit from 1,024 to 4,095 (THREADS=4095) and the initial swap
file size from 2 MB to 32 MB
(SWAPPATH=C:\OS2\SYSTEM 2048 32768).
- Tower TowerJ 2.2.7 Linux
-
- 19 October 1998 Tower Technology TowerJ Compiler 2.2.7.0
- Red Hat Linux Intel 5.2 (Linux Kernel 2.0.36)
- TowerJ Compiler
"version 2.2.7.0 x86-linux"
- Installed from
TowerJ_2_2_7_0_x86_linux.class
(8,633,718 bytes) and TowerJ_2_2_7_0_x86_linux_glibc.tar.gz
(4,663,423 bytes).
- Uses green threads and native pre-compilation.
- Used command option
-b-heap-min 8388608 to set initial
heap size to 8 MB, and built executable with TowerJ project files
Main.tj and
Mark.tj.
- IBM JDK
1.1.7 Windows part of SockPerf package
-
- 28 January 1999 IBM Win32 Java Virtual Machine Version 1.1.7
- Microsoft Windows NT Workstation Version 4.0 (Build 1381: Service Pack 4)
- java full version
"JDK 1.1.7 IBM build n117p-19990128 (JIT enabled: ibmjitc)"
- Installed from
ibm117gm.exe (10,132,439 bytes).
- Uses native threads and
ibmjitc just-in-time compiler.
- Microsoft SDK 3.1 Windows
-
- 20 January 1999 Microsoft SDK 3.1 Windows Build 3165
- Microsoft Windows NT Workstation Version 4.0 (Build 1381: Service Pack 4)
- jview version
5.00.3165
- Installed from
MSJavx86.exe (6,601,968 bytes) and
SDK-Java.exe (10,675,448 bytes).
- Uses native threads and just-in-time compiler.
- Heap and stack command line options are not available.
- Sun JDK 1.2 Windows
-
- 01 December 1998 Sun JDK 1.2 Windows 95/98/NT Production Release (Final)
- Microsoft Windows NT Workstation Version 4.0 (Build 1381: Service Pack 4)
- java full version
"JDK-1.2-V"
- Installed from
jdk12-win32.exe (20,521,166 bytes).
- Uses native threads and
symcjit just-in-time compiler.
- The heap options must be preceded by the capital letter
"X", and the stack options are not available.
- Novell JDK 1.1.5 NetWare
-
- 25 January 1999 Novell JVM for NetWare
- Novell NetWare Version 5.00
- java full version
"1.1.5" (JVM dated 01/08/99)
- Installed from
jvm.exe (18,107,065 bytes).
- Uses native threads and
symcjit just-in-time compiler
(Symantec Java! JustInTime Compiler Version 3.00.040(x) for JDK 1.1.x).
- Set Maximum Packet Receive Buffers = 1000 (default is 500).
- Modified the
java.cfg file to enable the just-in-time
compiler (JAVA_COMPILER=symcjit).
- Sun JDK 1.2 Solaris early access Java VM
-
- 22 December 1998 Sun JDK 1.2 Solaris Production Release (Early Access)
- Sun Solaris 7 Desktop Intel Platform Edition
- java full version
"Solaris_JDK_1.2_01_dev06_fcsV"
- Installed from
Solaris_JDK_1.2_01_dev06.i386.tar.Z
(26,411,384 bytes).
- Uses native threads and
sunwjit just-in-time compiler.
- The heap and stack options must be preceded by the capital letter
"X".
- Blackdown JDK 1.1.7 Linux
-
- 04 November 1998 Blackdown JDK 1.1.7 Linux
- Red Hat Linux Intel 5.2 (Linux Kernel 2.0.36)
- java full version
"Linux_JDK_1.1.7_v1a_green_threads"
- Installed from
jdk_1.1.7-v1a-glibc-x86.tar.gz
(12,219,873 bytes) and tya12v3.tgz (124,041 bytes).
- Uses green threads and
tya (TYA 1.2v3) just-in-time compiler.
- JDK 1.1.7 FreeBSD
-
- 21 December 1998 JDK 1.1.7 for FreeBSD
- FreeBSD 2.2.8-RELEASE
- java full version
"jdk1.1.7-FreeBSD:1998/12/21"
- Installed from
jdk1.1.7.V98-12-21.tar.gz (12,920,606
bytes).
- Uses green threads and no just-in-time compiler.
- Transvirtual Kaffe 1.00 Linux beta Java VM
-
- 08 December 1998 Transvirtual Technologies Kaffe OpenVM 1.0 Beta 3
- Red Hat Linux Intel 5.2 (Linux Kernel 2.0.36)
- Kaffe Virtual Machine
"Engine: Just-in-time Version: 1.00 Java Version: 1.1"
- Installed from
kaffe-1.0.b3-3.i386.rpm (1,192,063 bytes).
- Uses green threads and
kaffe.jit just-in-time compiler.
- Heap options require a space before the size
(-ms 8m -mx
64m).
Conclusion
If I had my way, there would be only one chart in this report, but I
guess that would make for a very short article. I believe the best
single test for determining the best Java server platform is to compare
the performance at a single high connection count. With this test, we
have a clear winner:
| Java platform |
Score at 2,100 connections |
Sun JDK 1.2 Solaris
Sun JDK 1.2 Windows
IBM JDK 1.1.7 Windows
Microsoft SDK 3.1 Windows |
1183
622
498
459
|
Figure 3. VolanoMark 2.1.2 network scalability test at
2,100 concurrent connections, measuring throughput in messages per
second. All tests ran identical copies of VolanoMark 2.1.2 on identical
hardware. Only Java VMs that passed the test are shown; the Sun and
Microsoft Java VMs for Windows required a reduction in their stack
reserve in order to pass the test.
After seeing Microsoft's Java VM lead the pack in Java server
performance and scalability for so long, I had started to think the
other Java vendors would never catch up. It seems I greatly
underestimated IBM's and Sun's ability to meet the challenge!
About the author
John Neffenger is the founder and chief technology officer of Volano
LLC. He is the author of VolanoChat, a Web-based chat solution written
in 100% Pure Java on both the client and server side. Volano has sold
more than 500 server licenses in 33 countries around the world, with
its largest customer averaging more than 69,000 hours of active connections
to more than 134,000 chat visitors per day. Prior to his role at Volano,
John was a software developer at IBM Corp., working on Taligent's
CommonPoint Application Development Toolkit for OS/2 Warp. Before his
assignment at Taligent, John worked in Palo Alto, CA, and Rome, Italy,
on IBM's Open Systems Interconnection protocol stack. John has a BA in
Mathematics from Northwestern University.
|