In 2006, I was working at Palm. One of my job duties was writing a PDF reader for Foleo, an ARM and Linux-powered mini laptop. The Palm management decided that PDF reader is a must have application. I ended up being the sole developer on the project.

Writing a PDF rendering library is a multi-year effort. We didn't have years, so I used Poppler open-source library. My job was to write basic PDF viewer that used Poppler to render PDF pages into a bitmap in memory and blit those bitmap on screen.

PDF is a complex format and rendering of some PDFs is slow. I wanted to improve the speed. The way to improve speed is to profile the code. Unfortunately, the toolchain for an unreleased ARM hardware wasn't very good. Forget about a profiler, be grateful you have a C++ compiler and don't have to enter assembly by typing hex.

Windows had decent profilers, so I compiled Poppler for Windows. Once I had the library working on Windows, I wrote simplest GUI app that would show the pages and allow navigating between pages. Now I had a simple PDF reader for Windows. I released it on my website. I tagged it as version 0.1.

Back to profiling : my plan worked. I profiled the documents that took the longest to render and made a few surprisingly simple and surprisingly effective optimizations. If memory serves, there are two optimizations that had the biggest effect. First, by adding a small buffer inside string class to hold small strings inline -- as opposed to always allocating memory for the string. Strings were used frequently and most of them were small. Two, fixing one-byte-at-a-time I/O process by converting it to bulk reads. Calling c read() function for each byte is extremely cheap, but not when you do it 5 million times.

At some point, I decided to switch from Poppler to mupdf, because it was better and actively maintained. But changing the app to use completely different library is not something you can do in an afternoon. It's demoralizing to work long time on code that doesn't even compile. To keep things compiling while also working towards supporting alternative rendering engine, I developed an abstraction for the rendering engine. The engine would provide the functionality the UI needed : getting number of pages in the document, sized of each pages (to calculate layout), rendering a page as a bitmap, etc. It served me well. I was able to incrementally convert this program from using Poppler to mupdf via engine abstraction. For a while, I supported both engines at the same time, but eventually I switched to just mupdf, to keep the app small.

It's about 127k lines of C++, written against Win32 API. By today's standards, SumatraPDF is tiny (installer smaller than 10 MB) and starts up instantly. How do I keep SumatraPDF small?

I avoid unnecessary abstractions. Windows is hard to program against, I could use wrappers like Qt, WxWindows or Gtk. They are easier to use, but it causes instant, giant bloat.

I'm also not afraid to write my own implementation of things. I have my own JSON, HTML/XML parsers that are a fraction of size of the popular libraries for those tasks. For storing advanced settings, I designed and implemented a file format that is smaller than XML, readable and writeable by humans and can be implemented in few hundred lines od code. It's as powerful as JSON and even more readable. Minimize the code size by not using STL? That's crazy, but I did it.

I learned about how Plan 9 OS source-code had non-traditional scheme of include files where they don't put ifdef wrappers in each h file (to allow multiple inclusion) and their h files don't include other h files. As a result, c files have to include every h file they need and in the right order. I did it and I keep doing it. It prevents circular dependencies between h files and doesn't inflate C++ build times because of careless including the same files over and over again.
— Krzysztof Kowalczyk (July 25, 2021) Lessons learned from 15 years of SumatraPDF