Custom blockchain development company in USA

Custom blockchain development company in USA

Step 6 - Performance analysis

In the last section we put the InsertStringBuffer class through much more rigorous testing and found some problems that were not tested by the original test suite. Now, if we were to add another implementation of a StringBuffer algorithm, we would already have a much more precise description of the kind of things that this class would need to do. (On the other hand, the tests in the last section were specifically targeted on weaknesses of a particular implementation. A new implementation with different weaknesses might break under different conditions. That's where a general Brute-Force test might come in handy again.)
Further, we could now refactor the InsertStringBuffer class without the fear that this would break the code.

However, the original reason for writing InsertStringBuffer was to fix the performance problem of the ConcatStringBuffer algorithm. After all, both classes provide the same logical functionality, so if performance was irrelevant (and in most cases it is), ConcatStringBuffer would be completely sufficient. More than that, it would be preferrable to InsertStringBuffer, since it is much simpler, and therefore less likely to contain bugs, as the last step has demonstrated.

Manually interpreted performance check

So let's have some fun and compare the run-time performance of the two classes. Add a new fixture class called "Step6", register it in the TestSuite, and add this code to it:

'Step6.cls

Option Explicit

Implements IFixture

Private m_assert As IAssert
Private m_timer As PerformanceCounter
Private m_stringBuffer As IStringBuffer

Private Const NumPowers As Long = 6
Private m_taskSize(NumPowers) As Long

Private Sub IFixture_Setup(assert As IAssert)
Dim l As Long
  Set m_assert = assert
  Set m_timer = New PerformanceCounter
  m_assert.EnablePrintMsg
  
  For l = 0 To NumPowers
    m_taskSize(l) = 2 ^ (l + 5)
  Next l
End Sub

Private Sub IFixture_TearDown()
End Sub

Private Sub FillBuffer(ByVal numIterations As Long)
Dim l As Long
  m_timer.Start
  
  For l = 0 To numIterations
    m_stringBuffer.Append "This is the " & l & _
               "nd line in the string." & vbCrLf
  Next l
  
  m_timer.Finish
  
  m_assert.PrintMsg vbTab & numIterations & vbTab & " Strings: " & _
      vbTab & Round(m_timer.MilliSeconds, 3) & vbTab & " ms", "msg1"
  Set m_stringBuffer = Nothing    'be sure to clear the buffer
End Sub


Public Sub TestConcatStringBuffer()
Dim l As Long
  For l = 0 To NumPowers
    Set m_stringBuffer = New ConcatStringBuffer
    FillBuffer m_taskSize(l)
  Next l
End Sub

Public Sub TestInsertStringBuffer()
Dim l As Long
  For l = 0 To NumPowers
    Set m_stringBuffer = New InsertStringBuffer
    FillBuffer m_taskSize(l)
  Next l
End Sub

This code just fills a StringBuffer with a specified number of Strings and measures how long it takes. This is a performance test. It is not a unit test, since it requires a human to read and interpret its results. We could turn it into a unit test by asserting certain properties of the run-time performance, for example that processing twice as many strings will require less than 2.5 times as much time. But for now, let's just look at the results:

Step6.TestConcatStringBuffer 32  Strings:  0.397  ms
Step6.TestConcatStringBuffer 64  Strings:  1.054  ms
Step6.TestConcatStringBuffer 128  Strings:  4.505  ms
Step6.TestConcatStringBuffer 256  Strings:  20.089  ms
Step6.TestConcatStringBuffer 512  Strings:  79.743  ms
Step6.TestConcatStringBuffer 1024  Strings:  322.873  ms
Step6.TestConcatStringBuffer 2048  Strings:  1490.938  ms
Step6.TestConcatStringBuffer 4096  Strings:  8416.406  ms
Step6.TestConcatStringBuffer 8192  Strings:  49201.083  ms
Step6.TestConcatStringBuffer 16384  Strings:  209781.177  ms
Step6.TestConcatStringBuffer 32768  Strings:  872458.042  ms
        
Step6.TestInsertStringBuffer 32  Strings:  0.352  ms
Step6.TestInsertStringBuffer 64  Strings:  0.646  ms
Step6.TestInsertStringBuffer 128  Strings:  1.275  ms
Step6.TestInsertStringBuffer 256  Strings:  2.502  ms
Step6.TestInsertStringBuffer 512  Strings:  4.973  ms
Step6.TestInsertStringBuffer 1024  Strings:  10.069  ms
Step6.TestInsertStringBuffer 2048  Strings:  21.995  ms
Step6.TestInsertStringBuffer 4096  Strings:  48.367  ms
Step6.TestInsertStringBuffer 8192  Strings:  105.545  ms
Step6.TestInsertStringBuffer 16384  Strings:  215.142  ms
Step6.TestInsertStringBuffer 32768  Strings:  436.549  ms

Your results will of course be different, depending on the power of your computer, but the main point here is the relative performance of the two algorithms: Processing 2 times as many strings takes 4-6 times as much time for the ConcatStringBuffer, while it only takes a little more than 2 times as much time for the InsertStringBuffer (A formal analysis of the InsertStringBuffer algorithm would show that its run-time is a little worse than linear, since the time required for growing the buffer gets worse as the buffer gets larger). In absolute terms, ConcatStringBuffer needs 32 times as much time as InsertStringBuffer for processing 1024 strings, and 2000 times as much time for processing 32000 strings.

However, for less than 64 strings, the performance of ConcatStringBuffer seems totally adequate when compared to the faster implementation, so for most tasks where only a small number of strings is to be concatenated, using the traditional str = str & "some text" seems very reasonable.

Automatic (unsupervised) performance check

Should the Step6 fixture be a permanent part of our TestSuite for the StringBuffer class? That depends on how important this particular performance requirement is, and how much time each run of the tests should take. If it should indeed be part of every run, I would decrease the number of processed strings to a point where the whole test runs very quickly, so that it does not slow down the development cycle.
Further, the test should be fully automatic, so that no human interpretation of the results is required. A minimal check could be that InsertStringBuffer is faster than ConcatStringBuffer for the same amount of strings. A more rigorous test could check certain statistical properties of the performance curve.
Since the whole motivation of writing the InsertStringBuffer class was to have something that is faster than the iterative concatenation algorithm, one could argue that the performance is indeed part of the specification, and that it should therefore be part of the test suite. This will allow to work on the implementation without the fear that some change will introduce another performance hog.

So let's turn this into an automatic unit test. Create a new fixture called "Step6a.cls" and add this code to it:

'Step6a.cls

Option Explicit

Implements IFixture

Private m_assert As IAssert
Private m_timer As PerformanceCounter
Private m_stringBuffer As IStringBuffer

Private Const NumPowers As Long = 6
Private m_taskSize(NumPowers) As Long
Private m_concatResults(NumPowers) As Double
Private m_insertResults(NumPowers) As Double

Private Sub IFixture_Setup(assert As IAssert)
  Set m_assert = assert
  Set m_timer = New PerformanceCounter
  m_assert.EnablePrintMsg
  
  Dim l As Long
  For l = 0 To NumPowers
    m_taskSize(l) = 2 ^ (l + 5)
  Next l
End Sub

Private Sub IFixture_TearDown()
End Sub

Private Function CompareMessage(ByVal expected As Double, _
                          ByVal actual As Double) As String
  CompareMessage = "Expected less than " & Round(expected, 3) & _
                   ", but was: " & Round(actual, 3)
End Function

Private Function FillBuffer(ByVal numIterations As Long) As Double
Dim l As Long
  m_timer.Start
  
  For l = 0 To numIterations
    m_stringBuffer.Append "This is the " & l & _
               "nd line in the string." & vbCrLf
  Next l
  
  m_timer.Finish
  
  m_assert.PrintMsg numIterations & " Strings: " & _
                    Round(m_timer.MilliSeconds, 3) & " ms", "msg1"
  Set m_stringBuffer = Nothing    'be sure to clear the buffer
  
  FillBuffer = m_timer.MilliSeconds
End Function


Public Sub TestConcatStringBuffer()
Dim l As Long
Dim ratio As Single
  For l = 0 To NumPowers
    Set m_stringBuffer = New ConcatStringBuffer
    m_concatResults(l) = FillBuffer(m_taskSize(l))
  Next l
End Sub

Public Sub TestInsertStringBuffer()
Dim l As Long
Dim ratio As Single
  For l = 0 To NumPowers
    Set m_stringBuffer = New InsertStringBuffer
    m_insertResults(l) = FillBuffer(m_taskSize(l))
    m_assert.Verify m_insertResults(l) < m_concatResults(l), _
                 "compare Concat and Insert", vbUnit_Assertion
    If l > 0 Then
      If m_insertResults(l - 1) <> 0 Then
        ratio = m_insertResults(l) / m_insertResults(l - 1)
        m_assert.PrintMsg "Ratio: " & ratio
        m_assert.Verify ratio < 2.5, "checkRatio" & l & ": " & _
                    CompareMessage(2.5, ratio), vbUnit_Assertion
      End If
    End If
  Next l
End Sub

Make sure to add this fixture to the test suite. You can now also remove the original Step6 from the test suite, since we are turning the experimental performance test into a fully automatic unit test. Hence, your test suite should now look like this:

'vbUnitTutorialSuite.Suite

Private Function ISuite_Suite() As ITest
Dim suite As New TestSuite
  suite.SuiteName = "vbUnit Tutorial Suite"
  
  suite.AddFixture New TestStringBuffer
'  suite.AddFixture New Step6
  suite.AddFixture New Step6a
  
  Set ISuite_Suite = suite
End Function

The code in Step6a.cls fills an array with the performance results of the ConcatStringBuffer implementation:

m_concatResults(l) = FillBuffer(m_taskSize(l))

Then it runs InsertStringBuffer with the same tasks and asserts that it is always faster than ConcatStringBuffer:

m_assert.Verify m_insertResults(l) < m_concatResults(l), "compare Concat and Insert", vbUnit_Assertion

Further, the unit test checks that doubling the task size increases run-time by not more than 2.5 times:

m_assert.Verify ratio < 2.5, "checkRatio" & l & ": " & CompareMessage(2.5, ratio), vbUnit_Assertion

Note that the ratio will eventually get larger than 2.5 as the task size increases, since the InsertStringBuffer algorithm does not quite run in linear time, so this assertion holds only for a specific range of task sizes. If you want to play around with this, you should disable the ConcatStringBuffer test (since it will just take too much time), and then increase NumPowers to make the task size for the InsertStringBuffer larger. Also, this test is not totally deterministic, since Windows is not a real-time operating system. Sometimes your computer may be busy with some background tasks while running this test, and the performance measurement will fail. However, for most runs you should get something like this:

   OK (8 Tests, 29 Assertions)  

   MESSAGES: 20   
Step6a.TestConcatStringBuffer : 32 Strings: 0.407 ms
Step6a.TestConcatStringBuffer : 64 Strings: 1.232 ms
Step6a.TestConcatStringBuffer : 128 Strings: 4.942 ms
Step6a.TestConcatStringBuffer : 256 Strings: 20.784 ms
Step6a.TestConcatStringBuffer : 512 Strings: 81.331 ms
Step6a.TestConcatStringBuffer : 1024 Strings: 320.021 ms
Step6a.TestConcatStringBuffer : 2048 Strings: 1402.208 ms
Step6a.TestInsertStringBuffer : 32 Strings: 0.363 ms
Step6a.TestInsertStringBuffer : 64 Strings: 0.646 ms
Step6a.TestInsertStringBuffer : Ratio: 1.777692
Step6a.TestInsertStringBuffer : 128 Strings: 1.35 ms
Step6a.TestInsertStringBuffer : Ratio: 2.09087
Step6a.TestInsertStringBuffer : 256 Strings: 2.551 ms
Step6a.TestInsertStringBuffer : Ratio: 1.889694
Step6a.TestInsertStringBuffer : 512 Strings: 4.98 ms
Step6a.TestInsertStringBuffer : Ratio: 1.952141
Step6a.TestInsertStringBuffer : 1024 Strings: 10.334 ms
Step6a.TestInsertStringBuffer : Ratio: 2.075175
Step6a.TestInsertStringBuffer : 2048 Strings: 24.718 ms
Step6a.TestInsertStringBuffer : Ratio: 2.391944

Severity Levels

There are two more things to note here. The first is the additional parameter given to the assertion:

m_assert.Verify ratio < 2.5, "checkRatio" & l & ": " & CompareMessage(2.5, ratio), vbUnit_Assertion

This optional parameter modifies the Severity Level for this assertion. The severity determines what happens if an assertion fails. By default, the severity level is vbUnit_Method. This means, after a failed assertion, execution terminates the current TestMethod and proceeds to the next one. The severity could also be higher: If it is set to vbUnit_Fixture, a failed assertion will terminate the current Fixture. A failed assertion with the severity level vbUnit_AllTests will completely stop the TestRunner. This may be useful for checking whether a required resource is available without which all further tests would be meaningless.
But it is also possible to decrease the severity level of an assertion. Setting the severity to vbUnit_Assertion means that a failed assertion will not exit the current test method. Instead, execution will continue at the next line within the same test method. In this test we are lowering the severity level because the test may fail once in a while, and we would still like to continue testing to see what the remaining iterations of the loop will do. For an example of this, just run this test a few times. Before long, you should get something like this:

   FAILURES: 1  
Step6a.TestConcatStringBuffer : 32 Strings: 0.408 ms
Step6a.TestConcatStringBuffer : 64 Strings: 1.059 ms
Step6a.TestConcatStringBuffer : 128 Strings: 4.555 ms
Step6a.TestConcatStringBuffer : 256 Strings: 20.086 ms
Step6a.TestConcatStringBuffer : 512 Strings: 79.85 ms
Step6a.TestConcatStringBuffer : 1024 Strings: 314.986 ms
Step6a.TestConcatStringBuffer : 2048 Strings: 1408.038 ms
Step6a.TestInsertStringBuffer : 32 Strings: 0.358 ms
Step6a.TestInsertStringBuffer : 64 Strings: 0.638 ms
Step6a.TestInsertStringBuffer : Ratio: 1.781591
Step6a.TestInsertStringBuffer : 128 Strings: 1.272 ms
Step6a.TestInsertStringBuffer : Ratio: 1.99387
Step6a.TestInsertStringBuffer : 256 Strings: 2.546 ms
Step6a.TestInsertStringBuffer : Ratio: 2.001537
Step6a.TestInsertStringBuffer : 512 Strings: 7.746 ms
Step6a.TestInsertStringBuffer : Ratio: 3.042019
Step6a.TestInsertStringBuffer : Verify failed : checkRatio4: Expected less than 2.5, but was: 3.042
Step6a.TestInsertStringBuffer : 1024 Strings: 10.088 ms
Step6a.TestInsertStringBuffer : Ratio: 1.302258
Step6a.TestInsertStringBuffer : 2048 Strings: 22.729 ms
Step6a.TestInsertStringBuffer : Ratio: 2.253123

In this case, the time for processing 512 strings was 3.042 times longer than that for 256 strings, so the ratio was larger than the allowed 2.5. With a default severity level, the test method would have stopped at this point. But in this particular case, we tolerate the occasional failure and want to let the test run to its end, so the severity level is reduced.

Disabling PrintMsg

The second and last thing to note is the amount of messages that this test still produces. We are now finished playing with the performance of InsertStringBuffer and would just like to make this a permanent part of our test suite, without having to look at 20 lines of debug messages every time the tests are run. Not only do these messages become annoying and bothersome to read, they may even distract your attention from something important later on. So let's turn them off. This is easy, just comment out the line m_assert.EnablePrintMsg in the Setup method of the fixture:

'Step6a.Setup

Private Sub IFixture_Setup(assert As IAssert)
  Set m_assert = assert
  Set m_timer = New PerformanceCounter
'  m_assert.EnablePrintMsg
  
  Dim l As Long
  For l = 0 To NumPowers
    m_taskSize(l) = 2 ^ (l + 5)
  Next l
End Sub

Now all calls to PrintMsg in this fixture will be ignored. However, should you ever need to work again on this fixture, you can quickly turn them back on by uncommenting this line again. For now, run the tests again and you should just get something like this:

   OK (8 Tests, 29 Assertions)  

Summary

In this step we have manually tested some things with m_assert.PrintMsg. Later we have turned the manual tests into unit tests by making them fully automatic, which means that they require no further human interpretation.

  • If a test should be part of the test suite for a component, make it fully automatic and unsupervised.
  • Turn off PrintMsg when you are done with a component. You can always turn it back on later.
  • Each assertion has a severity level that determines where execution continues after a failed assertion.
  • The default severity level is vbUnit_Method, which means that the current test method will be terminated after a failed assertion. Hence, by default, a failed assertion is treated like an error.
  • The severity level can be changed with m_assert.SetSeverity, or by adding the optional last parameter to the assertion itself