by Eric Hosick (erichosick@interfacevision.com)
One question we’ve been asked is how fast is the Vision framework? To answer that, we’ve created parts (aka messages) that can be used to configure a web server from scratch. The results are shown below.
We ran siege on a single 15” macbook pro (C# mono):
sudo siege -b -c300 -r50 http://localhost:3050
with the following results:
Transactions: 15000 hits
Availability: 100.00 %
Elapsed time: 9.55 secs
Data transferred: 12.47 MB
Response time: 0.13 secs
Transaction rate: 1570.68 trans/sec
Throughput: 1.31 MB/sec
Concurrency: 199.18
Successful transactions: 15000
Failed transactions: 0
Longest transaction: 4.39
Shortest transaction: 0.00
the same siege test was ran by Centmin Mod with the following results:
Transactions: 15000 hits
Availability: 100.00 %
Elapsed time: 20.20 secs
Data transferred: 43.89 MB
Response time: 0.27 secs
Transaction rate: 742.57 trans/sec
Throughput: 2.17 MB/sec
Concurrency: 199.77
Successful transactions: 15000
Failed transactions: 0
Longest transaction: 7.43
Shortest transaction: 0.00
We realize the comparison is not apples to apples but we wanted to have something we can start comparing to.
Let’s look at the SipCoffee message-configuration that gave us these results.
SipCoffee is a message-oriented programming language. By that, we mean that all parts within the language are considered messages. A message contains some specific behavior and an aggregate of named-message (a class (message) with computed properties (named-messages)).
A program is created by composing messages. This can also be viewed as creating a data structure which contains both data and behavior (behavior and data-structures are inseparable: as they should be).
When a message is activated, the behavior composed within the message is executed. This may cause named-message (those computed properties of the message) to also active.
We call SipCoffee programs message-configurations because we’ve completely separated mechanisms from the business domain. Any changes you make in a program are directly related to the business domain.
In the message-configuration below, you can twiddle with the Web Server at any level. You can change queue sizes, time out periods, the server port and uri, the number of parts in a part pool, and even the cache size of the socket reader.
The following is the message-configuration of a very very simple, but complete, WebServer in only 50 lines of SipCoffee.
Scope ( scopeId -2
ins (
File ( named "Http200.Body.Html" fileUriStr "./Html/200.html" )
ArrayByteBuilder ( named "Http200.Header"
replace Byte ( byte 120 )
part ArrayByte ( withArrayByte byte ( 72 84 84 80 47 49 46 49 32 50 48 48 32 79 75 13 10 68 97 116 101 58 32 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 120 13 10 83 101 114 118 101 114 58 32 86 105 115 105 111 110 13 10 67 111 110 116 101 110 116 45 84 121 112 101 58 32 116 101 120 116 47 104 116 109 108 59 32 99 104 97 114 115 101 116 61 85 84 70 45 56 13 10 67 111 110 110 101 99 116 105 111 110 58 32 107 101 101 112 45 97 108 105 118 101 13 10 67 111 110 116 101 110 116 45 76 101 110 103 116 104 58 32 120 120 120 120 120 120 120 120 120 120 120 120 13 10 13 10 ) )
items ArrayList (
ins (
DateTimeNow ()
WithLength ( part ScopeGet ( withName "Http200.Body.Html" scopeId -2 ) )
) ) ) )
part SocketRouter ( named "restServ"
streamSource SocketListener ( backlogQueueSize 2000
ipEndPoint IPEndPoint ( port 3050
ipAddress DNSLookup ( hostEntryStr "localhost" )
)
)
socketFactory PartPool ( initialSize 500
item FactoryInstance (
part PartPoolDecorator (
part RunWorkerWithTimeOut ( waitTime 60000
part Scope (
ins (
Socket ( named "socket" )
ArrayByteBuffered ( named "requestBody" sizeGrowBy 1024 sizeInitial 1024 )
ScopeGet ( named "respHead" scopeId -2 withName "Http200.Header" )
ScopeGet ( named "respBody" scopeId -2 withName "Http200.Body.Html" )
HttpHeader ( named "httpHeader" )
)
part Array ( run true
ins (
SocketReaderHeaderBody ( autoOpen true
streamSource ScopeGet ( withName "socket" )
data ArrayByteBuffered ( sizeGrowBy 1024 sizeInitial 1024 )
body ScopeGet ( withName "requestBody" )
headerEncoder ScopeGet ( withName "httpHeader" )
buffer ArrayByteBuffered ( sizeInitial 1024 sizeGrowBy 1024 )
terminator ArrayByte ( withArrayByte byte ( 13 10 13 10 ) )
)
SocketWrite ( autoOpen false
streamSource ScopeGet ( withName "socket" )
data ScopeGet ( withName "respHead" )
)
SocketWrite ( autoOpen false
streamSource ScopeGet ( withName "socket" )
data ScopeGet ( withName "respBody" )
)
NamedMessageGet ( withName "close"
part ScopeGet ( withName "socket" callBehavior false )
) ) ) ) ) ) ) ) ) )
Example messages are Scope, File, ArrayByteBuilder, ArrayByte and Socket (all implemented as classes). Example named-messages are ins, named, sizeGrowBy and withName (all implemented as computed properties).
Let’s break the message-configuration down into smaller parts we can easily explain.
The SocketRouter message, when activated, listens to a data stream on a given port. Any new connections on that data stream are forwarded to another data stream for handling.
The SocketRouter has two named-messages:
The message-configuration for the streamSource named-message is very simple.
streamSource SocketListener ( backlogQueueSize 2000
ipEndPoint IPEndPoint ( port 3050
ipAddress DNSLookup ( hostEntryStr "localhost" )
)
)
The streamSource named-message is a SocketListener message with a backlogQueueSize named-message of a 2000 message (Yep! 2000 is also a message) and a configured ipEndPoint which is easy to figure out.
The socketFactory named-message is a bit more complicated. We could configure our Web Server to handle one request at a time. However, that isn’t very practical and scales worth shit. We need a way to allow messages to run on multiple threads at the same time.
To do this, we can use factories and part pools.
A FactoryInstance message, when activated, will return a copy of the message-configuration located in the part named-message.
Our message-configuration is as follows:
FactoryInstance (
part PartPoolDecorator (
...
)
)
In the above message-configuration, the FactoryInstance message creates a copy of a PartPoolDecorator and all messages configured in the PartPoolDecorator message. This is one of the advantages of programming using data structures as opposed to parameterized functions. Want to create a copy of a program? Just duplicate it (Functions are great at manipulating data structures but don’t make for very good data-structures in and of themselves).
We could implement our Web Server using only the FactoryInstance message-configuration. Each time a request comes in, a copy of the message-configuration to handle the request is made and ran in it’s own thread (more on that below). However, why keep making copies of our message-configuration for every request by a client? Allocating and freeing memory are expensive operations. We should be able to re-use our copied message-configurations. This is where the PartPool and PartPoolDecorator messages come into play.
The messages PartPool and PartPoolDecorator, together, allow any message-configuration to be re-used multiple times throughout the execution of a program.
The PartPool message is simple:
The PartPoolDecorator message, when activated, activates the message contained in the part named-message. When the configured-message in part finishes, the PartPoolDecorator message automatically returns itself to it’s part pool.
Using these two messages in conjunction with a factory, we can take any message-configuration and make it re-usable. This allows us to configure very scalable message-configurations.
Our specific message-configuration is as follows:
PartPool ( initialSize 500
item FactoryInstance (
part PartPoolDecorator (
part RunWorkerWithTimeOut (
...
) ) ) )
The PartPool message starts out with an initialSize named-message of 500. When the PartPool is first activated, the FactoryInstance message is activated 500 times (on as many threads as we can) and the results are placed in the PartPool message. The PartPool pulls a single message from it’s pool and activates it.
In this case, the PartPoolDecorator is configured to contain a RunWorkerWithTimeOut message. This means the PartPoolDecorator is actually activated in a new thread meaning the PartPool returns control, almost immediately, to it’s parent message: the SocketRouter message. The SocketRouter message is now ready to handle the next incoming request.
Here are where things get even cooler. The RunWorkerWithTimeOut message is able to run any message-configuration in a thread. Yep. Multi-threaded programming in SipCoffee is just that simple.
Take a quick look at the original message-configuration. What you will notice is that there are no functions, parameters or variables. In message-oriented programming, everything is a message. Even the context/scope that a message is activated in, usually defined with a language using { and } (Defining context in a language isn’t very data-structure friendly), is a message.
To support context, we have a Scope message to define the context a message-configuration runs in.
In our message-configuration we have the following Scope message configured:
part Scope (
ins (
Socket ( named "socket" )
ArrayByteBuffered ( named "requestBody" sizeGrowBy 1024 sizeInitial 1024 )
ScopeGet ( named "respHead" scopeId -2 withName "Http200.Header" )
ScopeGet ( named "respBody" scopeId -2 withName "Http200.Body.Html" )
HttpHeader ( named "httpHeader" )
)
part Array ( ... )
)
The part named-message contains an Array message (more on that later). The ins named-message contains 5 messages, something like variables, each with a unique name within that Scope’s context:
Here comes some programming power! A “variable” can be a single message like Socket or it can be another configured message! Yep! Just think of the power you have as a programmer. Variables are actually configured-messages!
Notice that, for this Scope message, the scopeId named-message is not defined. This means, the context of the Scope will be based on the current named thread. Yep! Even more POWER! We can run this message-configuration in as many threads as we want and there is no chance of the “variables” being accessed from multiple threads. You no longer need to worry about re-entrance or things being thread safe within this Scope (this isn’t the case when a Scope message is configured to be global).
In message-oriented programming, accessing a “Variable” is also done using messages. SipCoffee has no equals (=) operator. To access a “Variable” within the current scope, you use the ScopeGet message.
In this example:
ScopeGet ( named "respHead" scopeId -2 withName "Http200.Body.Html" )
The ScopeGet message locates and activates a message named “respHead” in a scopeId of -2. This is configured as follows:
Scope ( scopeId -2
ins (
File ( named "Http200.Body.Html" fileUriStr "./Html/200.html" )
)
)
Awesome! It looks like we have a “global variable” named ‘Http200.Body.Html’ that, when activated, reads from a file named ‘./Html/200.html’.
In this example:
SocketReaderHeaderBody ( autoOpen true
streamSource ScopeGet ( withName "socket" )
)
the streamSource named-message of the SocketReaderHeaderBody is a ScopeGet message. That message is configured to locate a message named “socket” configured as follows:
part Scope (
ins (
Socket ( named "socket" )
)
)
Even more Awesome! It looks like we have not configured the scopeId meaning that this Scope is based on the managed thread id. When the SocketReaderHeaderBody is activated within a managed thread, the Scope is also contained within that managed thread.
To activate messages in a given order, we can use an Array message setting the run named-message to a true message (Yep, even true is a message).
Array ( run true
ins ( ... )
)
Any messages in the ins named-message of the Array message are activated in the order they were inserted.
Let’s look at how the message-configuration works.
First, we activate a SocketReaderHeaderBody message:
SocketReaderHeaderBody ( autoOpen true
streamSource ScopeGet ( withName "socket" )
data ArrayByteBuffered ( sizeGrowBy 1024 sizeInitial 1024 )
body ScopeGet ( withName "requestBody" )
headerEncoder ScopeGet ( withName "httpHeader" )
buffer ArrayByteBuffered ( sizeInitial 1024 sizeGrowBy 1024 )
terminator ArrayByte ( withArrayByte byte ( 13 10 13 10 ) )
)
Second, we write to the socket the response header:
SocketWrite ( autoOpen false
streamSource ScopeGet ( withName "socket" )
data ScopeGet ( withName "respHead" )
)
Third, we write to the socket the response body:
SocketWrite ( autoOpen false
streamSource ScopeGet ( withName "socket" )
data ScopeGet ( withName "respBody" )
)
Finally, we close the socket:
NamedMessageGet ( withName "close"
part ScopeGet ( withName "socket" callBehavior false )
)
The NamedMessageGet message causes a named message to activate. In this case, the named-message close (implementation is a calculated property accessed using reflection) of that same socket is activated.
And, once all of that runs we “fall-up” the message-configuration and the PartPoolDecorator message returns us to the PartPool to be used again!
Message-oriented programming kicks ass. In 50 lines of code, we are able to create a scalable web server that is able to keep up with the big names. We haven’t even started to optimize our underlying technology and it was written in C#. Imagine if it was implemented in something like Rust (aww please add calculated properties rust people!).
We have a data-structure of messages that is also the behavior of the program. That makes the message-configuration very easily re-used in a multi-threaded environment. We aren’t allocating memory all the time and we are able to activate message-configurations without the overhead of locking and worrying about re-entrance. We get the speed of “single threaded” programming in a multi-threaded environment. Woo hoo!
If you find our work on message-oriented programming interesting, please follow us @interfaceVision and/or @erichosick.