I recently released an update to BlogCFC. One of the fixes includes a performance update that I'd like to detail. It isn't that big of a deal. In fact, I assume most folks will consider it quite obvious when they see it. But I missed it for years and figured it would be good to share. To give you a clue - the issue only came about when I updated to the latest ColdFish and also involved the large number of subscribers I have at the blog here. I had noticed for a while now that whenever I published a blog entry, it took a good 10-15 seconds to go through. Let me show you the code and tell me if you can see the issue:
<cffunction name="mailEntry" access="public" returnType="void" output="false"
hint="Handles email for the blog.">
<cfargument name="entryid" type="uuid" required="true">
<cfset var entry = getEntry(arguments.entryid,true)>
<cfset var subscribers = getSubscribers(true)>
<cfset var theMessage = "">
<cfset var mailBody = "">
<cfloop query="subscribers">
<cfsavecontent variable="theMessage">
<cfoutput>
<h2>#entry.title#</h2>
<b>URL:</b> <a href="#makeLink(entry.id)#">#makeLink(entry.id)#</a><br />
<b>Author:</b> #entry.name#<br />
#renderEntry(entry.body,false,entry.enclosure)#<cfif len(entry.morebody)>
<a href="#makeLink(entry.id)#">[Continued at Blog]</a></cfif>
<p>
You are receiving this email because you have subscribed to this blog.<br />
To unsubscribe, please go to this URL:
<a href="#getRootURL()#unsubscribe.cfm?email=#email#&token=#token#">#getRootURL()#unsubscribe.cfm?email=#email#&token=#token#</a>
</p>
</cfoutput>
</cfsavecontent>
<cfif instance.mailserver is "">
<cfmail to="#email#" from="#instance.owneremail#" subject="#variables.utils.htmlToPlainText(htmlEditFormat(instance.blogtitle))# / #variables.utils.htmlToPlainText(entry.title)#" type="html">#theMessage#</cfmail>
<cfelse>
<cfmail to="#email#" from="#instance.owneremail#" subject="#variables.utils.htmlToPlainText(htmlEditFormat(instance.blogtitle))# / #variables.utils.htmlToPlainText(entry.title)#"
server="#instance.mailserver#" username="#instance.mailusername#" password="#instance.mailpassword#" type="html">#theMessage#</cfmail>
</cfif>
</cfloop>
</cffunction>
(Note, I removed a few lines that weren't relevant.) See the issue? Notice that I loop over the subscribers query. Remember that for my blog here, that query was 300+ users. I loop over every single user and for each, I'm generating a rendered blog entry. Imagine a blog entry with 4 code blocks in it. I ended up running ColdFish's rendering code 1200 times. Ouch.
I rewrote the code to simply pull out the function calls, like so:
<cfset var renderedText = renderEntry(entry.body,false,entry.enclosure)>
<cfset var theLink = makeLink(entry.id)>
<cfset var rootURL = getRootURL()>
<cfloop query="subscribers">
<cfsavecontent variable="theMessage">
<cfoutput>
<h2>#entry.title#</h2>
<b>URL:</b> <a href="#theLink#">#theLink#</a><br />
<b>Author:</b> #entry.name#<br />
#renderedText#<cfif len(entry.morebody)>
<a href="#theLink#">[Continued at Blog]</a></cfif>
<p>
You are receiving this email because you have subscribed to this blog.<br />
To unsubscribe, please go to this URL:
<a href="#rooturl#unsubscribe.cfm?email=#email#&token=#token#">#rooturl#unsubscribe.cfm?email=#email#&token=#token#</a>
</p>
</cfoutput>
</cfsavecontent>
There are probably other ways to do this too - but I guess the important take from this is - remember to pay special attention to loops. If you see yourself running some type of function within the loop, consider pulling that out (if possible) so you don't repeat the calls.
Archived Comments
There's no /cfloop in your second example.
Yes there is. Line 5. But code block 2 is just a subset.
What about:
<cfset var renderedText = renderEntry(entry.body,false,entry.enclosure)>
<cfset var theLink = makeLink(entry.id)>
<cfset var rootURL = getRootURL()>
<cfsavecontent variable="theMessage">
<cfoutput>
<h2>#entry.title#</h2>
<b>URL:</b> <a href="#theLink#">#theLink#</a><br />
<b>Author:</b> #entry.name#<br />
#renderedText#<cfif len(entry.morebody)>
<a href="#theLink#">[Continued at Blog]</a></cfif>
<p>
You are receiving this email because you have subscribed to this blog.<br />
To unsubscribe, please go to this URL:
<a href="#rooturl#unsubscribe.cfm?email=#email#&token=#token#">#rooturl#unsubscribe.cfm?email=#email#&token=#token#</a>
</p>
</cfoutput>
</cfsavecontent>
<cfloop query="subscribers">
<cfif instance.mailserver is "">
<cfmail to="#email#" from="#instance.owneremail#" subject="#variables.utils.htmlToPlainText(htmlEditFormat(instance.blogtitle))# / #variables.utils.htmlToPlainText(entry.title)#" type="html">#theMessage#</cfmail>
<cfelse>
<cfmail to="#email#" from="#instance.owneremail#" subject="#variables.utils.htmlToPlainText(htmlEditFormat(instance.blogtitle))# / #variables.utils.htmlToPlainText(entry.title)#"
server="#instance.mailserver#" username="#instance.mailusername#" password="#instance.mailpassword#" type="html">#theMessage#</cfmail>
</cfif>
</cfloop>
@Gary: Token is unique per subscriber. It allows me to unsubscribe myself from a entry thread. (Me being a subscriber.)
By the way - let me publicly thank Gary again. The "slow response to a new comment" issue he fixed has made me VERY happy!
p.s. Fixing to go to inlaws for a few hours so replies to comments will be delayed.
Ah, right. I missed the token. You could still save some time and memory by putting the message in the 'theMessage' and append the link with the token in the loop.
Before it took 10-15 sec. What impact has the fix done to the performance, how many seconds does it take now ?
0. :)
How about something like this?:
<cfset var renderedText = renderEntry(entry.body,false,entry.enclosure)>
<cfset var theLink = makeLink(entry.id)>
<cfset var rootURL = getRootURL()>
<!--- Let's local scope a few more variables --->
<cfset var theMessage = "">
<cfset var tmp = "">
<cfset var attr = StructNew()>
<!--- Then we'll set the mail body content that doesn't change --->
<cfsavecontent variable="theMessage"><cfoutput>
<h2>#entry.title#</h2>
<b>URL:</b> <a href="#theLink#">#theLink#</a><br />
<b>Author:</b> #entry.name#<br />
#renderedText#<cfif len(entry.morebody)>
<a href="#theLink#">[Continued at Blog]</a></cfif>
<p>
You are receiving this email because you have subscribed to this blog.<br />
To unsubscribe, please go to this URL:
</cfoutput></cfsavecontent>
<!--- Then we'll set the mail attributes that come from the instance, as they won't change on each iteration --->
<cfset attr.from = instance.owneremail>
<cfset attr.subject = variables.utils.htmlToPlainText(htmlEditFormat(instance.blogtitle)) & " / " & variables.utils.htmlToPlainText(entry.title)>
<cfset attr.type = "html">
<!--- We'll add the mail server, if we need it --->
<cfif Len(Trim(instance.mailserver))>
<cfset attr.server = instance.mailserver>
<cfset attr.username = instance.mailusername>
<cfset attr.password = instance.mailpassword>
</cfif>
<cfloop query="subscribers">
<cfset attr.to = email>
<!--- Then we'll concantonate our mail body with the subscriber specific info --->
<cfset tmp = theMessage & "<a href='#rooturl#unsubscribe.cfm?email=#email#&token=#token#'>#rooturl#unsubscribe.cfm?email=#email#&token=#token#</a></p>">
<!--- And use the attribute collection for the cfmail tag, mailserver or not --->
<cfmail attributeCollection="#attr#">#tmp#</cfmail>
</cfloop>
@Steve: Seems like someone else suggested that in comment # 6.
@Gary,
(Call me Cutter) Yeah, my feed aggregator hadn't gotten all the comments when I wrote the code block. I didn't see that when I posted.
Great minds... ;)
@Cutter: If only Ray was a programmer of our standard. LOL
Heh, I try...
If you are trying to match my standard, Stop It. We don't need you down here with me.
taking the other suggestions into account and using the closest code sample to this post, why not a cfthread right after
<cfloop query="subscribers">
That way, when the blog sends email to 3000 subscribers it will still run in about 0 seconds. No need to join them back, just let them run and die
BlogCFC runs on CF6, so cfthread isn't an option. I _can_ write code CF8 stuff (I've used cfthread for Ping), but it's a bit of a pain. As it stands, it takes 1 second now, so not worth the effort. ;)
DOH - forgot about backwards compatibility - not used to thinking about community distributions; that has got to get frustrating.
BlogCFC 6 (yes, I will build it, honest, why is everyone looking at me and snickering?) will drop 6, maybe even 7.
If you go with CF9 you can use the new features to keep up the database.
I'd _love_ to go with CF9, but I'd probably lose 99% of my customers. I've already lost a lot to Mango - don't want to lose the rest. ;)
Hmm, Manog. Heard of it... naw. I like BlogCFC way too much.