By the way, I fixed the code I had previously written to implement the exercise on p. 69, that we revised in the meeting on Nov. 19th.
This exercise was to take Stream.hs and rsa-pipeline.hs, and introduce rate limiting in the streaming by generating Fork constructions, which consumers would use to trigger the next chunk of data to be produced.
You can find the revised files in these gists:
You may recall that we spent the entire meeting fleshing out the streamMap function, only to find that when we ran it, it had a runtime error.
So, what was the problem? It turns out that when we handle the Fork constructor in streamMap, we forgot to fork! So some code ended up waiting on an IVar that never got computed. I'm not sure how it decided to error out at that point rather than blocking indefinitely.
There was also another bug, which caused duplicate entries in the output. The problem was an off-by-one error. The generator reaches the end of the chunk when i == chunkSize - 1, not when i == chunkSize.
How did I find these bugs? I instrumented the loops with calls to Debug.Trace.trace. That way I could print out values such as the loop index, or describe the case branch. The strict evaluation of the Par monad made it easy to guarantee the traces would be printed.
I also simplified the main loop. Instead of all the encrypt/decrypt on long strings, I just had it generate a stream from a small list, "abcd", and then do a streamFold that produced the result in reverse. I varied the chunk size and fork distance to produce edge cases. This is how I found the duplicate entry bug. But I was not getting the "no result" error.
So, that problem had to be caused by streamMap. So I added another step to my stream, that incremented the character. I found that if streamFromList was set to high values for chunkSize and forkDistance, then streamMap had no problem, even if it had to produce multiple chunks. The problem occurred when I set the chunkSize and forkDistance the same for both functions. Or if the forkDistance in streamFromList was smaller than the output length, and we used streamMap.
Therefore the problem was likely in the case where streamMap handled the Fork constructor. I examined the cases by eye, comparing the implementation of streamMap to streamFromList. This is where I noticed that streamMap was missing the call to fork a new thread. I fixed that, and everything worked.
In terms of run times, when the encryption/decryption runs on /usr/dict/words, the original program takes 18 seconds on my 2-core machine. With a chunkSize of 16 and forkDistance of 8, on all pipeline steps, the rate-limited version takes 13.5 seconds. With a chunkSize of 200 with a forkDistance of 100, it got down closer to 12 seconds. So, we must be gaining some efficiency from the rate-limiting. It'd be interesting to discuss why. The amount of garbage generated and collected seems to be about the same, so that doesn't explain it.
See you at the meeting tomorrow night!
- Lyle