Eamonn O'Brien-Strain


When thinking about your digital privacy it is important to consider what is the threat model that you are protecting against. You have to consider how important each is for you, and what you are willing to do to protect against it.

Privacy Threat Models

You can break down the data privacy threats into three layers, device, network, and servers.

And for each of these threats, you can consider

  1. What is the worst-case harm if your data was revealed to an unwanted party?
    • Mild annoyance (for example at ads you don't want)
    • General disquiet at being surveilled
    • Embarrassment, for example, someone finding you watching pornography
    • Being fired, for example in retaliation for labor organizing or whistle-blowing
    • Being prosecuted, for example by an authoritarian regime or by a US state enforcing regressive abortion laws
    • Being physically harmed or killed, for example, if you are the subject of intimate partner violence (domestic violence)
  2. What is the probability of that worst-case harm happening?
    • Are you a member of some vulnerable minority that has enemies?
    • Does your work mean you have highly valuable confidential information?
  3. Who are the stewards of your data? These are the people or institutions that have access to your data in the normal course of business.
  4. Who might be the unwanted parties who would cause you harm if they see your personal data?
  5. What mitigation can you do to reduce the probability of harm?
    • For device protection, you can use incognito or private browsing
    • For network protection, you can use a paid VPN (avoid most free VPNs, they actually reduce your privacy)
    • For server protection, use the privacy settings of each service to increase your privacy, and if you are in Europe reject consent for anything except essential cookies
  6. What is the cost of doing that mitigation, whether direct cost or reduction in usefulness of the service you are using?

As a summary, here is a framework for thinking about privacy threat models:

Device Network Servers
Data under threat on your physical device, or accessible in the cloud via your account in transit stored or logged in the cloud
Stewards of your data Apple, Samsung, Google, Mozilla, Firefox, app developers, ... coffee shop, airport, employer, Verizon, AT&T, Comcast, Akamai, Cloudflare, ... Google, Facebook, Amazon, TikTok, ...
Who might take your data family members, police, ... employer, prosecutors, government security services, ... prosecutors, government security services, hackers ...
Good Mitigation incognito mode, private browser, don't log in paid VPN, Tor don't log in, reject cookies, modify privacy settings
Cost of mitigation lose convenience of personalization monetary cost, reduced speed lose convenience of personalization

I'm writing this post in #WriteFreely a blogging platform that allows blogs to be followed and posts to be boosted on Mastodon.


Mastodon only handles plain text, so the examples below probably won't appear properly there.

WriteFreely allows for formatting such as

  1. italics
  2. bold
  3. lists like this
    • including
    • nested
    • lists
  4. headers like “Formatting” above
  5. console.log("inline code segments")

You can also block-quote text like this.

console.log("Or you can add code blocks")
console.log("like this.")


You can also embed images


and links to arbitrary pages.


WriteFreely posts can be arbitrarily long, as compared to Mastodon's limit of 500 characters.

  • Update: This is what the above part of the post looks like on Mastodon:*

Screenshot of the above post before expansion

Note, that the post is truncated so it is not longer than a typical Mastodon post. When you click the “Read more” it expands out to the following.

Screenshot of the above post after expansion

As expected, formatting like italics and bold are omitted, but the biggest loss is that embedded images are not shown.

Mastodon is great, but sometimes you want to write something long form without having to split it up into 500 character chunks. And sometimes you want a modicum of control over the formatting.

Now there is a new potential for a rebirth of the venerable blog form to complement Mastodon, while still being integrated with it in the larger Fediverse of federated servers.

This blog is an example. On Mastodon you can follow it by searching for


and clicking the Follow button. Then all future posts here will appear in your Mastodon feed.

(I'm still in the progress of converting over posts from the previous version of this blog, so you may see a bunch of metadata clutter the beginning of the older posts.)

I've long been passionate about issues of user privacy, so I jumped at the recent opportunity to spin up a new “Trust Experience” team in Google Search. Our team is responsible for aspects of the search user interfaces that affect user trust. (BTW, I'm hiring.)

I am excited about finally contributing to addressing what I think is the most difficult problem of privacy: how to give an average user transparency and meaningful control of their data, given the systems that manipulate the data are huge, complex, and opaque to almost everone.

I have a hypothesis that the key to solving this problem is to create a new kind of visualizable model that is a projection of how a user's data is being used.

Of course, our team has some mundane work to do too, including making sure the cookie consent dialogs meet all the regulatory mandates in different jurisdictions, and are not too annoying.

$$\text{width } 0.00391 \text{ centered at } -0.19854+i1.10018$$

I've now been using the Mandelbrot set images from my previous postings as my Google Meet video conferencing background for a few weeks, choosing a new one each day. It's been a fun conversation starter, and it's also indicative of what Googlers are like that a large proportion of people in my meetings know exactly what the image is.

I've now used up my first few batches of images. Time to find some more. Luckily there is an infinite number of them.

And what's more I've been doing some experiments with hill-shading to make the images reveal more of the structure, and also playing with the color palette so that the black of the actual Mandelbrot stands out better.

Shout out to @AnnaThieme for her article on coding up shaded relief for maps. With the help of her article I added the following to my C++ coloring code:

double hillshade(int ix, int iy) {
  // Values in the eight neighboring cells
  const double a = iterations(ix - 1, iy - 1);
  const double b = iterations(ix, iy - 1);
  const double c = iterations(ix + 1, iy - 1);
  const double d = iterations(ix - 1, iy);
  const double f = iterations(ix + 1, iy);
  const double g = iterations(ix - 1, iy + 1);
  const double h = iterations(ix, iy + 1);
  const double i = iterations(ix + 1, iy + 1);

  const double dzdx = ((c + 2 * f + i) - (a + 2 * d + g)) / (8 * KERNELSIZE);
  const double dzdy = ((g + 2 * h + i) - (a + 2 * b + c)) / (8 * KERNELSIZE);

  const double slope = atan(Z_FACTOR * sqrt(dzdx * dzdx + dzdy * dzdy));

  const double aspect = atan2(dzdy, -dzdx);
  double shade = ((cos(ZENITH) * cos(slope)) +
                  (sin(ZENITH) * sin(slope) * cos(AZIMUTH - aspect)));
  return shade < 0 ? 0 : shade;

Here are some new interesting areas of the Mandelbrot Set I've found

-0.7436438870371587 + i0.1318259042053119 width 5×10-13

video image

-0.18271806444477 + i0.66140756855431, width 1.45×10-11

video image

0.37001085813 + i0.67143543269, width 6×10-8

video image

-0.7345612674879727 + i0.3601896136089664, width 4.55×13-8

video image

0.1488658918 + i0.6424077239, width 5×13-7


-0.99920853376 + i0.30236435348, width 7.45×13-9


-0.7489856 + i0.055768, width 0.000244


And here are re-renderings of images from previous posts in this new style

Click the images to see at full resolution.

$$\text{width } 8 \text{ centered at } 0+i0$$

$$\text{width } 0.000244 \text{ centered at } -0.658448+i0.466852$$

$$\text{width } 0.000122 \text{ centered at } -0.715182+i0.2300282$$

$$\text{width } 0.000977 \text{ centered at } 0.284390+i0.013590$$

$$\text{width } 0.000977 \text{ centered at } 0.284430+i0.012732$$

$$\text{width } 4.657 \times 10^{-10} \text{ centered at } -0.13997533734+i0.992076239092$$

$$\text{width } 0.000977 \text{ centered at } -0.796186+i0.183227$$

$$\text{width } 1.19 \times 10^{-7} \text{ centered at } 0.250006+i0$$

$$\text{width } 7.45 \times 10^{-9} \text{ centered at } -1.9999117502+i0$$

$$\text{width } 0.00391 \text{ centered at } -0.19854+i1.10018$$

$$\text{width } 0.0001 \text{ centered at } -0.745263-i0.113042$$

$$\text{width } 2.5 \times 10^{-10} \text{ centered at } -0.749988802386+i0.006997251233$$

$$\text{width } 10^{-9} \text{ centered at } -1.67440967428+i0.00004716557$$

$$\text{width } 4.88 \times 10^{-13} \text{ centered at } -1.674409674652718+i0.000047165698791$$

$$\text{width } 2 \times 10^{-13} \text{ centered at } -1.674409674652720+i0.000047165698793$$

$$\text{width } 0.00000381 \text{ centered at } -1.28422516+i0.42732560$$

0.4464254440760-i0.4121418810742 2.328×10<sup>-10</sup> wide

-1.292628079931202-i0.352667377259980 7.28×10<sup>-12</sup> wide

0.4177429339294358+i0.2102828457812882 2.27×10<sup>-13</sup> wide

-0.8624892205832114+i0.21478927144381935 2.27×10<sup>-13</sup> wide

0.4177429339294358+i0.2102828457812882 2.27e-13 wide

Generating more Mandelbrot set details and videos zooming down to them.

See previous article for more details on this.

0.4464254440760-i0.4121418810742 2.328×10-10 wide:

video image

-1.292628079931202-i0.352667377259980 7.28×10-12 wide:

video image

0.4177429339294358+i0.2102828457812882 2.27×10-13 wide:

video image

-0.8624892205832114+i0.21478927144381935 2.27×10-13 wide:

video image

$$\text{width } 0.00391 \text{ centered at } -0.19854+i1.10018$$


More than thirty years ago an exhibition in London changed how I view the world.

It was called “Schönheit im Chaos / Frontiers of Chaos”, presented by the Goethe-Institut, the German cultural organization. I still have the exhibition book by H.O. Peitgen, P. Richter, H. Jürgens, M. Prüfer, and D.Saupe

Schönheit im Chaos

It upended what I thought I knew about mathematics. It was astounding how much complexity could emerge from extremely basic calculations.

I was particularly drawn to the uncanny beauty and complexity of the Mandelbrot set, which is the complex values $$c$$ for which the iteration $$z \gets z^2 + c$$ does not diverge when starting with $$z = 0$$. This is so simply stated1, and yet generates the images below, revealing endless variations of complexity as you zoom deeper into the set.

Early Difficulties

Over the years I made several attempts to generate these images myself, on the various workstations and PCs I had available at the time. But ultimately it was frustrating because each image took so long to compute that it was painfully slow to explore to any depth in the Mandelbrot set.

The reason for the long compute times is that for each of the $$c$$ values of a million or so pixels you need to iterate $$z \gets z^2 + c$$ until it is diverging ($$\lvert z \rvert > 2$$). That can be fairly fast for pixels not in the Mandelbrot set (displayed with a color according to how many iterations). But for pixels that are in the Mandelbrot set (displayed as black), they will never diverge, so you have to pick some maximum number of iterations before giving up and deeming it to be non-diverging. The big problem is pixels that are outside the set but very close to it: they might require a large number of iterations before they diverge, especially at high zoom levels. To get a reasonably accurate set boundary you need the maximum iterations threshold to be at least 1000 at moderate zoom and 10,000 or 100,000 at higher zooms. Thus you can easily be doing billions of complex-number calculations per image.

In the end the computers I had available at the time were just too slow.

Trying Again

During the 2020 Winter Holidays I started thinking about generating Mandelbrot sets again, for the first time in more than a decade.

I realized that one thing different was that even my now several-years-old Linux ThinkPad laptop was vastly faster than anything I tried using before, especially if I could use all eight cores in parallel.

The first question was what programming language to use. To get the performance I wanted, interpreted languages like Python and JavaScript were out of the question, and I also thought that VM languages like Java or a .Net languages probably would not give me the control I wanted. So that left the choices:

  1. Rust is on my to-do list of languages to learn, but it was more than I wanted to tackle over the Holidays. Also the memory-safety features, which are the big advantages of Rust, while important for large-scale programming are not so important for this rather modest application.
  2. Go would have been fine, but in my past experience that libraries for graphics and color handling are fewer and harder to use than for C++.
  3. C++ is what I ended up choosing. Carefully written it can be faster than anything except possible assembly-language programming (and I was not willing to go down that rathole)

The only other performance option I considered was to investigate using GPU computation — but I decided to leave that for another time.

As a first quick prototype preserved in the now-obsolete SDL branch of the code on Github I used the SDL graphics library to throw up a window on my laptop and set pixels.

int iterations(int maxIterationCount, const complex<double> &c) {
  complex<double> z = 0;
  for (int i = 0; i < maxIterationCount; ++i) {
    z = z * z + c;
    if (abs(z) > 2) {
      return i;
  return maxIterationCount;

The C++ code takes advantage of the convenience of being able to use the std::complex type which means that the code z = z * z + c looks pretty much like the mathematical expression $$z \gets z^2 + c$$.

The code uses double which I've found allows zooming down to widths of $$10^{-13}$$ before obvious numeric artifacts appear. A possible future extension of the project would be to use arbitrary precision math to allow deeper zooms.

I also made sure I was using all available processing power by running multiple threads in parallel, which was easy because each pixel can be calculated independently. The threads interleave which rows they are working on:

int threadCount = thread::hardware_concurrency();

void threadWorker(const Params &params, Image *img, int mod) {
  for (int iy = mod; iy < params.imgHeight; iy += threadCount)
    for (int ix = 0; ix < params.imgWidth; ++ix) {
      img->iterations(ix, iy) = iterations(params, ix, iy);
  cout << "Finished thread " << mod << endl;


  vector<thread> threads;
  for (int mod = 0; mod < threadCount; ++mod) {
    threads.emplace_back(threadWorker, params, &img, mod);
  for (auto &thread : threads) {

The good news was this experiment showed reasonable performance, generating megapixel images in seconds.


However this prototype had no UI (it had hardcoded parameters) so I needed to figure out how to put a UI on it. What I chose was a web app that operates as follows:

  1. The user opens a URL with the parameters encoded in the URL hash parameters.
  2. The client JS decodes the parameters and sets the src attribute of the <img> tag to a URL that invokes the server JS.
  3. If the image file for these parameters has not already been generated, the server JS invokes the C++ binary, which generates the image file and writes it out to the server's static files directory.
  4. The server JS sends back a 302 response to the client, redirecting it to the the static URL for the generated image file.
  5. The client JS updates its UI, and its hash parameters (enabling browser forward and back history to work).
  6. The user chooses some parameters and clicks on somewhere in the image.
  7. The client JS updates the src attribute of the <img> tag according to the updated parameters to update the URL that invokes the server JS. (goto step 3)

app.get('/image', (req, res) => {
  const { x, y, w, i } = req.query
  const imgPath = `/cache/mandelbrot_${x}_${y}_${w}_${i}.png`
  const imgFileName = `public${imgPath}`

  if (existsSync(imgFileName)) {
    console.log('Using existing cached ', imgFileName)
  console.log('Generating ', imgFileName)
  const ls = spawn('./mandelbrot', [
    '-o', imgFileName,
    '-x', x,
    '-y', y,
    '-w', w,
    '-i', i,
    '-W', imgWidth,
    '-H', imgHeight

  ls.on('close', code => {
    console.log(`child process exited with code ${code}`)
    console.log('Generated ', imgFileName)

The server-side JavaScript code is very straightforward. It provides one endpoint for serving the static image files and a /image endpoint for invoking the C++ binary and redirecting to the generated image file:

let busy = false

const setCursorMagnification = () => {
  const magnification = magnificationElement.valueAsNumber
  zoomElement.innerText = 1 << magnification
  imgElement.className = `cursor-${magnification}`

const setIterations = () => {
  const i = Math.pow(10, iLog10Element.valueAsNumber)
  iNewElement.innerText = i

const doit = () => {
  if (!window.location.hash) {
    window.location = '#0_0_8_1000'
  const [x, y, w, i] = window.location.hash.substr(1).split('_')
  xElement.innerText = x
  yElement.innerText = y
  wElement.innerText = w
  iElement.innerText = i
  busy = true
  imgElement.className = 'cursor-busy'
  imgElement.setAttribute('src', `/image?x=${x}&y=${y}&w=${w}&i=${i}`)
  imgElement.onload = () => {
    busy = false
  imgElement.onclick = event => {
    if (busy) {
      imgElement.className = 'cursor-error'
      setTimeout(() => {
        if (busy) {
          imgElement.className = 'cursor-busy'
      }, 200)
    const { offsetX, offsetY } = event
    const scale = w / imgWidth
    const height = scale * imgHeight
    const viewPortLeft = x - w / 2
    const viewPortTop = y - height / 2
    const newX = viewPortLeft + offsetX * scale
    const newY = viewPortTop + (imgHeight - offsetY) * scale
    const magnification = magnificationElement.valueAsNumber
    const zoom = 1 << magnification
    zoomElement.innerText = zoom
    window.location = `#${newX}_${newY}_${w / zoom}_${iNewElement.innerText}`

window.onload = () => {
  window.onhashchange = doit
  magnificationElement.onchange = setCursorMagnification
  iLog10Element.onchange = setIterations

The client JavaScript code has the plumbing to pass down the parameters from the HTML to the server via the image URL query parameters. It also has a simple mutex mechanism using the busy Boolean to prevent the page spawning more than one concurrent image generation, and to modify the cursor to let the user know the state.


Click any image to see in full resolution, $$1920 \times 1080$$, which is suitable for use as a video-conferencing background.

$$\text{width } 8 \text{ centered at } 0+i0$$

$$\text{width } 0.000244 \text{ centered at } -0.658448+i0.466852$$

$$\text{width } 0.000122 \text{ centered at } -0.715182+i0.2300282$$

$$\text{width } 0.000977 \text{ centered at } 0.284390+i0.013590$$

$$\text{width } 0.000977 \text{ centered at } 0.284430+i0.012732$$

$$\text{width } 4.657 \times 10^{-10} \text{ centered at } -0.13997533734+i0.992076239092$$

$$\text{width } 0.000977 \text{ centered at } -0.796186+i0.183227$$

$$\text{width } 1.19 \times 10^{-7} \text{ centered at } 0.250006+i0$$

$$\text{width } 7.45 \times 10^{-9} \text{ centered at } -1.9999117502+i0$$

$$\text{width } 0.00391 \text{ centered at } -0.19854+i1.10018$$

$$\text{width } 0.0001 \text{ centered at } -0.745263-i0.113042$$

$$\text{width } 2.5 \times 10^{-10} \text{ centered at } -0.749988802386+i0.006997251233$$

$$\text{width } 10^{-9} \text{ centered at } -1.67440967428+i0.00004716557$$

$$\text{width } 4.88 \times 10^{-13} \text{ centered at } -1.674409674652718+i0.000047165698791$$

$$\text{width } 2 \times 10^{-13} \text{ centered at } -1.674409674652720+i0.000047165698793$$

$$\text{width } 0.00000381 \text{ centered at } -1.28422516+i0.42732560$$

1 Though it is a bit more complex when the complex numbers are decomposed into their real and imaginary components: $$ z \gets z^2 + c $$ where $$ z = x + i y $$ $$ c = a + i b $$ expands to $$ x \gets x^2 -y^2 + a$$ $$ y \gets 2 x y + b $$

Looking at today's per-capita death rates:

  1. In the world rankings, the Latin American countries are still the worst affected, with some improvement for Mexico relative to the others.
  2. The worst seven US states are now Arizona and six Southern states, with Florida moving into fourth place.
  3. Three Arizona counties have moved into the top four of all counties in the US amongst the worst four along with one Mississippi county.
  4. In California, Orange County has moved up into the top three behind Los Angeles and Imperial Counties.

States with highest per-capita Covid death rates

Some highlights from today's graphs of per-capita Covid death-rates:

  1. In the ranking of countries, Latin America is doing worst, taking up the top seven spots. The United States is not far behind in the middle of the pack, with the United Kingdom and Sweden not doing much better. The rest of Europe is now doing pretty well, which is quite a turnaround from earlier in the pandemic.
  2. In the US, looking at the ranking by state or by county shows what has been much reported in the news, that states which escaped the initial impact of the pandemic now have the highest ranking, lead by Arizona and Mississippi. But some of the states initially hit hard, particularly New Jersey are also seeing high death rates again. New York is now doing quite well, at the bottom of the ranking, but California, which had done well in the past is now climbing up worryingly out of the middle of the pack.
  3. In the ranking of California counties, Imperial county is still by far the worst affected, but this is off a very small population base. Of the counties with large populations there is a big divide between NorCal and SoCal, with the southern counties all in the high up in per-capita deaths and the Bay Area counties all low down.

Oh yes, in an ideal world we would use a debugger to understand what our code is doing, but setting up a development environment to allow debugging can sometimes be tricky or annoying to do.

So often we resort to the venerable art of “printf debugging”: adding judiciously placed print statements in our code.

In JavaScript this means simply adding console.log statements.

But if you are writing code in a functional style, it can sometimes mess up your code to add print statements.

For example imagine you have a bug in some code that looks like this ...

const dotProd = (v1, v2) =>
  zip(v1, v2).map(pair => pair[0] * pair[1]).reduce((a, x) => a + x, 0)

... and you want to inspect the value of a + x returned by the reducer.

To use console.log you would have to rewrite the reducer to this inelegant form:

const dotProd = (v1, v2) =>
  zip(v1, v2).map(pair => pair[0] * pair[1]).reduce((a, x) => {
    const result = a + x
    console.log('Reducer returns', result)
    return result
  }, 0)

This really makes it inconvenient and error-prone to add these temporary logging statements, and to remove them when you're done.

So I published the pp function as a more elegant alternative.

To use it in Node JS, install with

npm install passprint

and then import it into your JavaScript file like so:

const { pp } = require('passprint')

and then you can do

const dotProd = (v1, v2) =>
  zip(v1, v2).map(pair => pair[0] * pair[1]).reduce((a, x) => pp(a + x), 0)

Note the additional pp(...) near the end. This is much less ugly than using console.log.

This will log out lines that look something like:

|||||||||MyClass.myFunction myFile.js:46:63 22ms 78878

where 22ms is the time elapsed since logging started and 78878 is the value of a + x. The number of | characters shows the depth of the call stack.

The pp simply returns the value passed to it, so it can be used with minimal perturbation to your code. It is easy to add and remove.

You don't need any extra disambiguating message with pp because the log messages include the function name, file name, and line number where it was invoked from.

More details in the README on Github.

Feel free to give feedback on Twitter or by adding a GitHub issue.