EMF Rendering and Persisted Streams

EMF rendering in Reporting Services has an interesting history.  When SSRS first shipped, we did not support "direct printing" from within the web interface.  Customers revolted.  They wanted a "Print" button on the toolbar which would provide a full featured print experience.  Many other web applications don't actually do this, choosing instead to present the user with a "printable" HTML page or some other format such as PDF which they then print within their PDF reader.  Rather than this workaround though, it was decided that we would implement an EMF renderer and provide a client-side ActiveX control which would send the EMF to the print spooler.

Turns out EMF has an interesting characteristic though.  Each page is a discrete stream.  Normally, this wouldn't cause a problem, but due to how renderers were architected, the cost restarting the renderer on a specific page is a function of which page it is within the report.  Specifically, when you request page N of the report from the EMF renderer, it needs to find page N, which means essentially internally walking across pages 1...N-1 in order to position itself on page N.  A quick back of the envelope calculation will tell you then that the cost of printing a report by requesting each page independently has an algorithmic complexity of

Where R(i) is the time to independently render a page, and T(i) is the time it takes to "paginate through" a given page without actually writing anything to the response stream.  This is not good.  Adjusting the way that the EMF renderer operated would require too much of a redesign to our processing and rendering architecture, so it was decided to instead address this issue by adding a new feature to the server.  This feature was "Persisted Streams."  When this mode is enabled, all of the requested streams are rendered in one pass from the EMF renderer so we don't incur the cost of restarting the renderer on a particular page.  The server returns the initial request when the first page is completed, and continues in the background to collect pages from the renderer and store them until they are requested by the client.  From the clients perspective, you merely keep requesting pages until the server returns an empty stream, which indicates that the report has finished rendering.  There are a couple of limitations to using the Persisted Streams feature though:

  1. It is only accessible via the URL Access endpoint, and not via our SOAP APIs.  Unfortunately, this is what happens when features are implemented in a service pack.  It was only needed for a very narrow scenario, so from an implementation perspective we did not choose to incur the cost of making it available to our SOAP API.
  2. It is kind of a "stateful" API, meaning that once you have read a stream, you cannot read it again.  So make sure you get it right the first time because you can't go back.

 

Here is some sample code that demonstrates how use our URL Access endpoint to retrieve streams in this fashion: 

             string requestUri =
                string.Format("https://{0}/reportserver?{1}&rs:Command=Render&rs:Format=IMAGE&rc:OutputFormat=EMF",
                m_serverName, m_reportPath);

            const string persistStreams = "&rs:PersistStreams=true";
            const string getNextStream = "&rs:GetNextStream=true";
            CookieContainer cookies = new CookieContainer();
            WebRequest request = WebRequest.Create(requestUri + persistStreams);
            (request as HttpWebRequest).CookieContainer = cookies;
            try
            {
                while (true)
                {
                    request.UseDefaultCredentials = true;
                    
                    using (WebResponse response = request.GetResponse())
                    {
                        Stream s = response.GetResponseStream();
                        BufferedStream buffered = new BufferedStream(s);
                        if (OnReceivedStream(buffered))
                        {
                            break;
                        }

                        request = WebRequest.Create(requestUri + getNextStream);
                        (request as HttpWebRequest).CookieContainer = cookies;
                    }
                }
            }
            catch (WebException ex)
            {                
                OnReceivedException(ex);
            }

Obviously, this code is not 100% complete.  It is part of a larger piece of a WinForms application that I have written which excercises this functionality.  It should be enough to get you started though.