Hi, I'm Weiran Zhang. I work as a Software Enginnering Manager at Capital One. I have a passion for iOS and building thriving software teams. This blog is a place for me to document things I've learned and things I find interesting. You should follow me on Twitter.
When I got my 27” iMac in 2017 it came with 8GB of RAM. Being one of the few Apple Macs with user upgradable RAM1, I decided to avoid that part of the Apple tax and buy my own. The stock 8GB came in two 4GB sticks to enable dual channel memory. The iMac had 2 spare slots, so I ordered 16GB more as two 8GB sticks.
It was only when I opened the package that I realised I bought a single 16GB stick instead of two 8GB sticks. This would give the iMac three sticks RAM, which meant it couldn’t use dual channel memory.
Being impatient, instead of exchanging it for what I really wanted, I kept it and thought I would just buy another 16GB stick when the price is lower.
It turned out that for over 18 months RAM prices would remain above what I paid for my original 16GB stick. I didn’t really use all of the 24GB of RAM I had, so I decided to keep on waiting. I never really felt like my iMac was slow2, and didn’t think single channel memory would make much of difference.
Recently that the price dropped to a point where it didn’t feel like I was just burning money to satisfy a curiosity,
So now I had dual channel memory again, is there any real difference?
I ran Geekbench 4 just before and after installing the final 16GB RAM stick, and I was surprised to see over 10% increase with dual channel memory. Single-core test resulted in 5,799 vs 5,249, and multi-core 20,403 vs 17,485. The detailed results show small increases across the tests, with memory bandwidth almost doubling. Certain multi-core tests had a big difference, probably ones which were being limited by the memory bandwidth rather than CPU performance.
I also tried testing the time it took to do a clean full archive of Hackers, but it was only faster by one second3 with dual channel memory. Apart from playing games4, compiling swift is probably the most taxing thing I do on this Mac. Adding that new memory stick will make no noticable difference to that.
Does dual channel memory make a difference? Only if you’re doing something that is constrained by memory bandwidth. If you don’t know if you’re doing something that is memory bandwidth constrained, that means you probably aren’t. I’ve spent £60 and gained nothing except a slightly better Geekbench score, but your mileage may vary.
The others being the Mac mini and the new Mac Pro. You can upgrade the RAM in the iMac Pro but it involves cutting sticky tap holding the display to the chassis and a fairly lengthy disassemble process to get the motherboard out. I’ve done this before on an old 2011 iMac that needed GPU replacing, but I don’t think I’d have the guts to do it on a brand new £5,000 iMac Pro.
↩This is the 2017 model with the i7-7700K which for its time was an incredibly fast processor. Even now the single threaded performance is pretty good, and the iMac has the cooling to let it turbo boost to 4.4GHz pretty regularly. Although the noise the iMac’s fans make when at full load is a different story.
↩34 vs 35 seconds with single channel memory. This is well within the margin of error as my timing methodology relied on using the iPhone’s timer app. I think it also shows the synthetic nature of Geekbench, and doesn’t always represent real world usage.
↩I should’ve ran some gaming benchmarks before so I could do some comparisons, but my gut feeling is that most recent games are constrained by the Radeon Pro 580 GPU rather than the CPU or memory.
↩One of the biggest challenges in developing Hackers is generating and fetching thumbnails for each post. The early versions didn’t have a thumbnail, but scrolling through a long list of posts without a visual indicator meant it was really easy to lose your position. Adding a thumbnail image really helped with spatial awareness.
But neither the Hacker News API nor website have thumbnails for posts, only text links. I had to find another way.
The first implementation in Hackers fetched each post body in the list. With the raw HTML, it would then scour the HTML for images that looked like they could be thumbnails. Once it found the one it wanted, it would then fetch the image, resize it, and display it in the cell. This would have to be done for each and every post in the list, of which 30 loaded at a time. Obviously this was incredibly slow, and taxing on your iPhone’s CPU and data connection.
My algorithm for parsing HTML for images wasn’t particularly efficient either, and in the days of the iPhone 5S and 6, the app would sometimes struggle to get 60 FPS scrolling, especially when the images it picked turned out to be large and the resizing would start to bottleneck.
There were some advantages to this approach, it was all done on the device so no server side component to host or manage. But the cost was probably too much work for a mobile device, and complex asynchronous code that would have to chain together multiple network requests, and a synchronous queue to conditionally update cells that could potentially (and probably likely) to have already gone off screen.
I never really liked this, but honestly felt too invested in the monster I had created to let it go. I had several attempts at making it more efficient, introduction better local caching of thumbnails, and request cancellation if the cell disappeared off screen. But they didn’t fix the fundamental problems with the approach, and just made the code even more complicated. It was time for a rethink.
My second attempt would borrow from an old idea in Hackers. Even older versions of Hackers used an orchestration API I hosted on Heroku that converted the Hacker News website HTML into JSON response. I eventually moved this to in-app as the performance hit was negligible and would enable authentication and voting features in the future.
However thumbnail fetching would be much easier if I created an API that took a URL as a parameter, and it did the fetching of the HTML body, parsing it, and returning a 301
redirect response to the image. This considerably reduced the work in-app, and made thumbnails faster and easier to fetch. A simple stateless API is much easier to scale on cloud infrastructure, so I created a simple Node.js service, and hosted on the Zeit Now platform on their free tier. I really felt much happier about this approach I’d offloaded the appropriate work to a server side component while still keeping the core functionality working in-app. I could also remove a whole heap of code that I was never liked and hated changing.
But a pretty big problem still remained: the images it fetched were more often than not way too large to be a thumbnail and but I still had to download the whole image (sometimes several megabytes), and then resize and crop it into a thumbnail to display.
I had a sudden realisation that our browsers now show high quality thumbnails, and not just the old 32x32 favicon. Through a combination of Apple Touch Icon and Open Graph, most web pages now have a high quality thumbnail embedded in their HTML response.
And of course, there are already several packages on npm that do just that. So Hackers now displays better thumbnail images that are much smaller in size, and the best part was I didn’t even need to ship an update to Hackers to do it, it was just a change to the API.
Robbie Trencheny pointed me to a new Apple framework quietly released at WWDC called LinkPresentation. The official documentation is sparse, but Apple released a WWDC session video explaining the basics.
This is the same framework that Messages.app uses to show link previews. Swinjective-C has a good summary, but essentially everything is wrapped in LPMetadataProvider
which does all the heavy lifting for you:
let metadataProvider = LPMetadataProvider()
metadataProvider.startFetchingMetadata(for: url) { (metadata, _) in
if let metadata = metadata {
}
}
Does this mean I can actually remove my custom thumbnail fetch from Hackers? I wanted to experiment so I created a branch for a proof of concept. In doing so I discovered that when you call startFetchingMetadata
, it actually creates a WKWebView
in the background to fetch and parse the whole URL. This adds a huge amount of overhead just to get a thumbnail, and is by far the most inefficient method I’ve tried in Hackers. LPMetadata
conforms to NSSecureCoding
so I added caching, but this only helped for subsequent requests. Just initiating around 10 requests at once for a single display’s worth of thumbnails would grind the app to a halt. It doesn’t help that you have to use LPMetadataProvider
on the main thread, as its unable to instantiate a WKWebView
on a background thread.
Unfortunately while LPMetadata
sounds good in theory, Apple’s implementation is geared much more towards rich previews and not just fetching thumbnails. In its current state there’s no way I can use it in Hackers.
While I’m happy with the current way of fetching thumbnails in Hackers, there is still a final optimisation I have in mind. The API is completely stateless, which means it has to fetch and parse every URL requested even if it has processed the same URL before. A simple cache or CDN in front of it could make it even faster. But first I want to focus on getting the iOS 13 update ready.