Wednesday, March 21, 2012

Problems with performance when programmatically printing large amounts of pages

We are using VB.NET 2005 to develop a WinForms application that uses SQL Server 2005 Reporting Services to produce reports.

Originally we tried using code found on Bryan's WebLog, http://blogs.msdn.com/bryanke/articles/71491.aspx, but found that for reports of 300 pages it was taking about 2 minutes to render each page.

When using RenderStream to render idividual pages, reports of about 20 pages printed without a problem, but for reports of 300 pages we recieved the follwoing error:

"The stream cannot be found. The stream identifier that is provided to an operation cannot be located in the report server database."

Is there some cache or something that we should be using? Any ideas appreciated.

-- Leah

The rendering code:

Public Function RenderReport(ByVal reportPath As String) As Byte()()

Dim deviceInfo As String = Nothing
Dim format As String = "IMAGE"
Dim firstPage As Byte() = Nothing
Dim encoding As String = ""
Dim mimeType As String = ""
Dim warnings As Warning() = Nothing
Dim reportHistoryParameters As ParameterValue() = Nothing
Dim streamIDs As String() = Nothing
Dim pages As Byte()() = Nothing

deviceInfo = String.Format("<DeviceInfo><OutputFormat>{0}</OutputFormat>{1}</DeviceInfo>", "emf", m_strDeviceInfo)

Try

firstPage = m_reportingService.Render(reportPath, format, Nothing, deviceInfo, m_parameterValues, Nothing, Nothing, encoding, mimeType, reportHistoryParameters, warnings, streamIDs)

m_numberOfPages = streamIDs.Length + 1
ReDim pages(m_numberOfPages - 1)
Dim iIndex As Integer = 0
pages(0) = firstPage
Dim pageIndex As Integer = 1

For pageIndex = 1 To m_numberOfPages - 1

' original page rendering from Bryan's blog
'deviceInfo = String.Format("<DeviceInfo><OutputFormat>{0}</OutputFormat><StartPage>{1}</StartPage{2}</DeviceInfo>", "emf", pageIndex + 1, m_strDeviceInfo)
'pages(pageIndex) = m_reportingService.Render(reportPath, format, Nothing, deviceInfo, m_parameterValues, Nothing, Nothing, encoding, mimeType, reportHistoryParameters, warnings, streamIDs)

' Attempt using streams, indexing in to pull back pages in order
iIndex = Convert.ToInt32(streamIDs(pageIndex - 1).Substring(streamIDs(pageIndex - 1).LastIndexOf("_"c) + 1))
pages(iIndex - 1) = m_reportingService.RenderStream(reportPath, format, streamIDs(pageIndex - 1), Nothing, deviceInfo, m_parameterValues, encoding, mimeType)

Next pageIndex

Catch ex As SoapException

m_strErrorMessage = ex.Message

Catch ex As Exception

m_strErrorMessage = ex.Message

End Try

Return pages

End Function

The best way to get streams for printing is to use URL access to render the report. Generate the url and for your first request pass in the first and last page page number (or nothing if you want all pages), as well as rs:PersistStreams=True. Then continue calling render with the same url only instead of passing rs:PersistStreams=True, pass in rs:GetNextStream=True. Do this until you get back an empty response.

Your first url should look like this:

http://<machinename>/ReportServer?<path to report>&<report params in format name=value>&rs:Command=Render&rs:format=IMAGE&rc:OutputFormat=emf&rc:StartPage=1&rc:EndPage=300&rc:PageWidth=11.0in&rc:PageHeight=8.500in&rc:MarginTop=6.350mm&rc:MarginBottom=6.350mm&rc:MarginLeft=6.350mm&rc:MarginRight=6.350mm&rs:PersistStream=True

This will cause Report Server to Render the entire report on the first request and then server back each page on subsequent request untill all pages have been exhausted. Your current code is attempting to render the entire report on each request which will take forever on large reports.

I hope that helps.

|||

Thanks for the reply.

This approach is very different to what we are doing (which as you say, was taking forever).

Could you give more information on how to use the url? Is it part of the web service?

|||

If you browse to http://<machine>/ReportServer, you are doing what is call Url access. That is viewing report items via a url (as opposed to using a SOAP call). To do this in you app, create a web request object and set the url to the one mentioned above. You can then send the request and read the response stream to get the data.

Look at the following msdn articles for creating and using web requests:

http://msdn2.microsoft.com/en-us/library/bw00b1dc.aspx

http://msdn2.microsoft.com/en-us/library/system.net.httpwebrequest.aspx

The code will be very similiar to your current code, since the SOAP proxy just wraps a WebRequest object. You need to set the url and the Credentials property to make sure you are properly authenticated.

I hope that helps.

|||

So there was no way to get the RenderStream code I posted above to work? :)

It is just that since it was tested as working with good performance for reports up to 100 pages. I thought there was some persistance / cache thing I might have been missing to make it work for large reports.

I will look through the links you provided. Will post back.

|||

Unable to get this to work (see below). My preference would be to be able to use the webservice.

In the meantime I have found some code on CodeProject that print a generated PDF. I will look at using this to get users going.

Thanks,
Leah

--
Details of problem with URL access

Code adapted from the links and related help topics.

Tested the code generated URL (strCreateUrl ) by pasting it into a browser and found we had a one page emf.

The ReceiveStream generated could not be indexed. To print using WinForms model we need a stream that we can index for page by page printing.

Code:
Dim strCreateUrl As String = "http://myServer/ReportServer?/myReportPath"
Dim myEnumerator As IEnumerator = m_parameterValues.GetEnumerator
Dim aParameterValue As ParameterValue

While myEnumerator.MoveNext()
aParameterValue = CType(myEnumerator.Current, ParameterValue)
strCreateUrl += "&" + aParameterValue.Name + "=" + aParameterValue.Value
End While

strCreateUrl += "&rs:Command=Render&rs:format=IMAGE&rc:OutputFormat=emf&rc:StartPage=1&rc:PageWidth=11.0in&rc:PageHeight=8.500in&rc:MarginTop=6.350mm&rc:MarginBottom=6.350mm&rc:MarginLeft=6.350mm&rc:MarginRight=6.350mm&rs:PersistStream=True"

Dim myRequest As System.Net.WebRequest = System.Net.WebRequest.Create(strCreateUrl)
myRequest.Credentials = System.Net.CredentialCache.DefaultCredentials

' Return the response.
Dim myResponse As System.Net.HttpWebResponse = CType(myRequest.GetResponse(), System.Net.HttpWebResponse)
Dim ReceiveStream As Stream = myResponse.GetResponseStream()

|||

Your code looks right, the problem you state is because you only call it once. Each call will only get you one page. Your next call should replace PersistStream=True with GetNextStream=True. You continue calling with GetNextStream=True until the response stream is empty. You can store each response as you would like (in the print control we actually store the response in temp files in case there are many pages).

I hope this helps.

|||

Do you have any sample code for this?

I have tried to quickly write code that will read through the next streams but I do not know how to detect the last stream.

Thanks,
Leah

__
Code attemp:

Dim strCreateUrl As String = http://myServer/ReportServer?/myReport
Dim myEnumerator As IEnumerator = m_parameterValues.GetEnumerator
Dim aParameterValue As ParameterValue

While myEnumerator.MoveNext()
aParameterValue = CType(myEnumerator.Current, ParameterValue)
strCreateUrl += "&" + aParameterValue.Name + "=" + aParameterValue.Value
End While

strCreateUrl += "&rs:Command=Render&rs:format=IMAGE&rc:OutputFormat=emf&rc:StartPage=1&rc:PageWidth=11.0in&rc:PageHeight=8.500in&rc:MarginTop=6.350mm&rc:MarginBottom=6.350mm&rc:MarginLeft=6.350mm&rc:MarginRight=6.350mm"

' call first page with persist as true
Dim myRequest As System.Net.WebRequest = System.Net.WebRequest.Create(strCreateUrl + "&rs:PersistStream=True")
myRequest.Credentials = System.Net.CredentialCache.DefaultCredentials

' Return the response.
Dim myResponse As System.Net.HttpWebResponse = CType(myRequest.GetResponse(), System.Net.HttpWebResponse)
Dim ReceiveStream As Stream = myResponse.GetResponseStream()

Dim iPageCounter As Integer = 0

While True

Try
myRequest = System.Net.WebRequest.Create(strCreateUrl + "&rs:GetNextStream=True")
ReceiveStream = myResponse.GetResponseStream()

If ReceiveStream Is Nothing Then
Exit While
End If

iPageCounter += 1
Catch ex As Exception
Exit While
End Try

End While

' Close the response to free resources.
myResponse.Close()

|||I don't have code in front of me to reference, but I believe all you need to check for is ReceiveStream.Length == 0.|||

Thanks for your answer.

Using ReceiveStream.Length == 0 caused an exception.

In the autos window the RecieveStream length property is shown as:
- Length {"This stream does not support seek operations."} Long

RecieveStream is actually of type System.Net.ConnectStream.

If you think Length property should work... do you think I should I cast the RecieveStream?

|||Ahh, trying to do this from memory is not working. :) Ok, one more try. If you read the stream, it should not have any length. I will try and dig up some code tomorrow.|||

Update on status.

Still have not gotten the URL Access technique (as suggested by Daniel) to work.

Further testing of the RenderStream approach shows that it can work on reports up to 240 pages, reports larger then this fail with follwoing SOAP Exception: "The stream cannot be found. The stream identifier that is provided to an operation cannot be located in the report server database"

Another team here has decided to use the RenderStream solution in their applicaiton because it appears to work for reports below 100 pages (100 pages is their largest report). My concern with this approach is that the reports do not appear to be consistantly persisting.

As a temporary fix for the applicaiton I am working on I have implemented a print solution that will render a PDF to file and create a process that uses Adobe to Print to the default printer.

Just writing this update I have had come up with some more ideas to try. I will let you know how I go. In the meantime if you can come up with more details on how you got the URL Access printing working it would be much appreciated. :)


_
Render Stream implementation (extract of code psoted in first post on this thread)

For pageIndex = 1 To m_numberOfPages - 1

' Attempt using streams, indexing in to pull back pages in order
iIndex = Convert.ToInt32(streamIDs(pageIndex - 1).Substring(streamIDs(pageIndex - 1).LastIndexOf("_"c) + 1))
pages(iIndex - 1) = m_reportingService.RenderStream(reportPath, format, streamIDs(pageIndex - 1), Nothing, deviceInfo, m_parameterValues, encoding, mimeType)

Next pageIndex

|||

Here is code I use to get the response:

responseLength = (int) response.ContentLength; // -1 means content length not sent so we must get the size.

if (responseLength == -1)

{

responseLength = 0;

using (Stream stream = response.GetResponseStream())

{

const int readSize = 64 * 1024;

int amountRead = 0;

byte[] input = new byte[readSize];

do

{

amountRead = stream.Read(input, 0, readSize);

responseLength += amountRead;

} while (amountRead > 0);

}

}

|||

Thanks for posting this code.

I am trying to work out where this fits in. I am assuming that your code is used to work out the amount of bytes in the stream so you can read the bytes in the stream into a page array of byte arrays. So your code would be used inside a loop which will GetNextStream while there are still pages.

Could you let me know if I am on the right track?

Thanks,
Leah


Code first draft (not working)

' call first page with persist as true
Dim myRequest As System.Net.WebRequest = System.Net.WebRequest.Create(strCreateUrl + "&rs:PersistStream=True")

myRequest.Credentials = System.Net.CredentialCache.DefaultCredentials

' Return the response.
Dim myResponse As System.Net.HttpWebResponse = CType(myRequest.GetResponse(), System.Net.HttpWebResponse)

While True

Try

Dim responseLength As Integer = CInt(myResponse.ContentLength) ' -1 means content length not sent so we must get the size.

If responseLength = -1 Then
responseLength = 0

' Using
Dim stream As Stream = myResponse.GetResponseStream
Try
Const readSize As Integer = 64 * 1024
Dim amountRead As Integer = 0
Dim input(readSize) As Byte

Do
amountRead = stream.Read(input, 0, readSize)
responseLength += amountRead
Loop While amountRead > 0

Finally
CType(stream, IDisposable).Dispose()
End Try

End If

' TO DO: add code here to read the amountRead of bytes into my page array of byte arrays

myRequest = System.Net.WebRequest.Create(strCreateUrl + "&rs:GetNextStream=True")
'myResponse = CType(myRequest.GetResponse(), System.Net.HttpWebResponse)

Catch ex As Exception
Exit While
End Try

End While

myResponse.Close()

|||

You don't need the TODO. You are already reading in the data. I should have explained that the sample I gave, did not care about the content (it was from a test) it only cared about the length. The code is throwing away the data currently.

So if the ContentLength is set, just create a byte array of the correct size, then do a single read to get the data. If not, in the loop you are reading the data, just store it where you want.

You exit the loop when responseLength is zero.

I hope that helps.

No comments:

Post a Comment