PyTorch and the Unknown CUDA Error

The Problem

I recently decided to get back into using neural networks again and tried to update my docker container to get fastai up and running, but couldn't get CUDA working. After a while spent trying different configurations of CUDA, pytorch, pip, conda, and on and on I eventually found out that there's some kind of problem with using CUDA after suspending and then resuming your system (at least with linux/Ubuntu). This is a documentation of that particular problem and it's fixes (fastest but not necessarily the best answer: always shutdown or reboot the machine, don't suspend and resume).

The Symptom

This is what happens if I try to use CUDA after waking the machine from a suspend.

import torch

torch.cuda.is_available()
/home/athena/.conda/envs/neurotic-fastai/lib/python3.9/site-packages/torch/cuda/__init__.py:88: UserWarning: CUDA initialization: CUDA unknown error - this may be due to an incorrectly set up environment, e.g. changing env variable CUDA_VISIBLE_DEVICES after program start. Setting the available devices to be zero. (Triggered internally at /opt/conda/conda-bld/pytorch_1666642975993/work/c10/cuda/CUDAFunctions.cpp:109.)
  return torch._C._cuda_getDeviceCount() > 0

As you can see, the error message doesn't really give any useful information about what's wrong - there are a couple of suggestions but neither seems relevant or at least doesn't lead you to the fix.

The Disease and Its Cure

There's a post on the pytorch discussion boards about this error in which "ptrblck" says that he runs into this problem if his machine is put into the suspend state. While mentioning this he also says that restarting his machine fixes the problem, but restarting it every time seems to defeat the purpose of using suspend (and I'd have to walk to a different room to log in and decrypt the drive after restarting the machine - ugh, so much work).

Luckily, in a later post in the thread the same user mentions that you can also fix it by reloading the nvidia_uvm kernel module by entering these commands in the terminal:

sudo rmmod nvidia_uvm
sudo modprobe nvidia_uvm

Which seems to fix the problem for me right at the moment, without the need to restart the machine.

print(torch.cuda.is_available())
False

Ummm… oops. Well, it did sort of fix one problem - the CUDA unknown error, but now it's saying that CUDA isn't available on this machine. Every fix begets a new problem. Let's try it again after restarting the Jupyter kernel.

import torch
print(torch.cuda.is_available())
True

Okay, that's better, I guess. It feels a little inelegant to have to do this, but at least it seems to work.

Tangling Multiple Org Files

I've been looking off and on for ways to combine separate code-blocks in org-mode into a single tangled file. I wanted to use it because I tangle code that I want to re-use out of posts but then if I want to break the posts up I need to create a separate file (tangle) for each post. I'm hopeful that this method will allow me to break up a tangle across multiple posts. I've only tried it on toy files but I want to get some initial documentation for it in place.

The Steps

Let's say that there are two source org-files:

  • one.org: contains the tangle block and a source block
  • two.org: contains another block that we want to tangle with the one in one.org

The steps are:

  1. Put an #+INCLUDE directive to include two.org into one.org
  2. Export one.org to an org file
  3. Open the exported org file (one.org.org)
  4. Tangle it.

Create one.org

The file one.org is going to have the tangle and the first source-block:

#+begin_src python :tangle ~/test.py :exports none
<<block-one>>

<<block-two>>
#+end_src
#+begin_src python :noweb-ref block-one
def one():
    print("One")
#+end_src

We also need to include what's in the second file (two.org). The code we want to include is in a section called Two so we can include just that section by adding a search term at the end.

#+INCLUDE: "./two.org::*Two"

Create two.org

In the other file add the section header to match the INCLUDE search term (*Two) and put a code block with a reference named block-two to match what's in the tangle block above.

* Two
#+begin_src python :noweb-ref block-two
def two():
print("Two")
#+end_src

Export one.org

Tangling unfortunately ignores the INCLUDE directive so we have to export it first to another org-file in order to get the text from org.two into our source file. By default, exporting to org is disabled so you need to enable it (e.g. starting with M-x customize org-export-backends).

Once it's enabled you can export one.org to an org-mode file using C-c C-e O v (the default name will be one.org.org).

Tangle one.org.org

The last choice when we exported the file in the previous step (v) will save it to a file and open it up in an emacs buffer. When the buffer is open you can then tangle it (C-c C-v C-t) and the output (/test.py from our tangle block) should contain both of our functions.

Sources

This is where I got the information on breaking up the files. It includes some emacs-lisp to run the steps automatically (although I didn't try it):

This is the post that mentions that exporting org-files to org-format needs to be enabled (and how to do it):

This is the manual page explaining the search syntax (which is what the #+INCLUDE format uses).

This explains the #+INCLUDE directive options:

CodeWars: Pick Peaks

Table of Contents

Beginning

The problem given is to write a function that returns the location and values of local maxima within a list (array). The inputs will be (possibly empty) lists with integers. The first and last elements cannot be called peaks since we don't know what comes before the first element or after the last element.

Code

Imports

# pypi
from expects import equal, expect

The Submission

def pick_peaks(array: list) -> dict:
    """Find local maxima

    Args:
     array: list of integers to search

    Returns:
     pos, peaks dict of maxima
    """
    output = dict(pos=[], peaks=[])
    peak = position = None

    for index in range(1, len(array)):
        if array[index - 1] < array[index]:
            position = index
            peak = array[index]
        elif peak is not None and array[index - 1] > array[index]:
            output["pos"].append(position)
            output["peaks"].append(peak)
            peak = position = None
    return output
expect(pick_peaks([1,2,3,6,4,1,2,3,2,1])).to(equal({"pos":[3,7], "peaks":[6,3]}))
expect(pick_peaks([3,2,3,6,4,1,2,3,2,1,2,3])).to(equal({"pos":[3,7], "peaks":[6,3]}))
expect(pick_peaks([3,2,3,6,4,1,2,3,2,1,2,2,2,1])).to(equal({"pos":[3,7,10], "peaks":[6,3,2]}))
expect(pick_peaks([2,1,3,1,2,2,2,2,1])).to(equal({"pos":[2,4], "peaks":[3,2]}))
expect(pick_peaks([2,1,3,1,2,2,2,2])).to(equal({"pos":[2], "peaks":[3]}))
expect(pick_peaks([2,1,3,2,2,2,2,5,6])).to(equal({"pos":[2], "peaks":[3]}))
expect(pick_peaks([2,1,3,2,2,2,2,1])).to(equal({"pos":[2], "peaks":[3]}))
expect(pick_peaks([1,2,5,4,3,2,3,6,4,1,2,3,3,4,5,3,2,1,2,3,5,5,4,3])).to(equal({"pos":[2,7,14,20], "peaks":[5,6,5,5]}))
expect(pick_peaks([18, 18, 10, -3, -4, 15, 15, -1, 13, 17, 11, 4, 18, -4, 19, 4, 18, 10, -4, 8, 13, 9, 16, 18, 6, 7])).to(equal({'pos': [5, 9, 12, 14, 16, 20, 23], 'peaks': [15, 17, 18, 19, 18, 13, 18]}))
expect(pick_peaks([])).to(equal({"pos":[],"peaks":[]}))
expect(pick_peaks([1,1,1,1])).to(equal({"pos":[],"peaks":[]}))

CodeWars: Simple Pig Latin

Description

Move the first letter of each word to the end of it, then add "ay" to the end of the word. Leave punctuation marks untouched.

Code

Imports

# pypi
from expects import equal, expect

Submission

import re

LETTERS = re.compile(r"[a-zA-Z]")
WORD_BOUNDARY = re.compile(r"\b")


def convert(token: str) -> str:
    """Convert a single word to pig-latin

    Args:
     token: string representing a single token

    Returns: 
     pig-latinized word (if appropriate)
    """
    return (f"{token[1:]}{token[0]}ay"
            if token and LETTERS.match(token) else token)


def pig_it(text: str) -> str:
    """Basic pig latin converter

    Moves first letter of words to the end and adds 'ay' to the end

    Args:
     text: string to pig-latinize

    Returns:
     pig-latin version of text
    """
    return "".join(convert(token) for token in WORD_BOUNDARY.split(text))
expect(pig_it('Pig latin is cool')).to(equal('igPay atinlay siay oolcay'))
expect(pig_it('This is my string')).to(equal('hisTay siay ymay tringsay'))
expect(pig_it("Hello World !")).to(equal("elloHay orldWay !"))

CodeWars: RGB To Hexadecimal

Description

Given three arguments r, g, and b which are integers from 0 to 255 representing RGB values, convert them to a six digit (zero-padded) hexadecimal string. Invalid (out of range) values need to be rounded to nearest value.

The Code

Imports

# pypi
from expects import equal, expect

Submission

HEX_DIGITS = "0123456789ABCDEF"
RGB_TO_HEX = dict(zip(range(len(HEX_DIGITS)), HEX_DIGITS))
HEX = 16

def rgb(r: int, g: int, b: int) -> str:
    """Convert RGB to Hexadecimal

    If the values are out of bounds they will be set to the nearest limit

    e.g. -1 becomes 0 and 2919 becomes 255

    Non-integers will raise an error

    Args:
     r: red channel (0-255)
     g: green channel (0-255)
     b: blue channel (0-255)

    Returns:
     6-digit hexadecimal equivalent of r, g, b
    """
    colors = (max(min(color, 255), 0) for color in (r, g, b))
    converted = ((RGB_TO_HEX[color//HEX], RGB_TO_HEX[color % HEX])
                 for color in colors)
    return "".join((y for x in converted for y in x))
expect(rgb(0,0,0)).to(equal("000000"))
expect(rgb(1,2,3)).to(equal("010203"))
expect(rgb(255,255,255)).to(equal("FFFFFF"))
expect(rgb(254,253,252)).to(equal("FEFDFC"))
expect(rgb(-20,275,125)).to(equal("00FF7D"))

Alternatives

A surprising number of people used the string formatting - {:02X} to convert the numbers to hexadecimal. I think that's sort of the problem with these earlier puzzles - there's a big question of how much of python's built in functionality you should use. I guess since I use string formatting a lot that might make sense as a shortcut in this case.

def rgb_2(r: int, g: int, b: int) -> str:
    """Convert RGB to Hexadecimal

    If the values are out of bounds they will be set to the nearest limit

    e.g. -1 becomes 0 and 2919 becomes 255

    Non-integers will raise an error

    Args:
     r: red channel (0-255)
     g: green channel (0-255)
     b: blue channel (0-255)

    Returns:
     6-digit hexadecimal equivalent of r, g, b
    """
    return "".join((f"{max(min(color, 255), 0):02X}" for color in (r, g, b)))
expect(rgb_2(0,0,0)).to(equal("000000"))
expect(rgb_2(1,2,3)).to(equal("010203"))
expect(rgb_2(255,255,255)).to(equal("FFFFFF"))
expect(rgb_2(254,253,252)).to(equal("FEFDFC"))
expect(rgb_2(-20,275,125)).to(equal("00FF7D"))

Not quite so readable, but short.

CodeWars: Rot13

Description

Given a string, replace each letter with the one that comes 13 letters after it in the alphabet. Ignore non-English aphabetical characters.

The Code

Imports

# python
from string import ascii_lowercase as lowercase
from string import ascii_uppercase as uppercase

# pypi
from expects import equal, expect

The Submitted Function

This is the version I submitted to CodeWars. It uses the dict update method to build a dict (although the solutions below seem neater) and the get method to handle the cases where the letter in the message isn't in the dictionary.

def rot13(message: str) -> str:
    """Implement a Caesar Cipher by shifting letters 13 places

    Non-english letters are left as-is

    Args:
     message: string to encode

    Return:
     the encoded version of the input string
    """
    code = {letter: lowercase[(index + 13) % 26] 
             for index, letter in enumerate(lowercase)}
    code.update((letter, uppercase[(index + 13) % 26])
                  for index, letter in enumerate(uppercase))
    return "".join(code.get(letter, letter) for letter in message)

A Test

def tester(encoder):
    inputs = ("test", "Test", "Test5")
    expecteds = ("grfg", "Grfg", "Grfg5")

    for message, expected in zip(inputs, expecteds):
        encoded = encoder(message)
        expect(encoded).to(equal(expected))
        expect(encoder(encoded)).to(equal(message))
    return

tester(rot13)

Alternatives

Quite a few of the other solutions (on the first page, anyway) used the built in str.maketrans and str.translate methods (they complement each other). I didn't see anything in the documentation about how defaults are handled so I'd have to look into it more. The top answer also used slicing instead of modulo (lower[13:] + lower[:13]) which might be better. The comments mention that the top answer actually won't work anymore since the maketrans and translate functions got moved out of string (which is where it's importing it from).

The top solutions seem to have a mix of current python and deprecated python (python 2?) so you'd have to be careful in using any of them.

Using the Slicing

If you were use slicing instead of the modulo I think it might look like this (from here on out I'm going to declare the code-books outside the function the way I would normally do it, I kind of didn't really think about it when submitting the solution above so I'll leave it as is).

CODE = dict(zip(lowercase + uppercase,
                lowercase[13:] + lowercase[:13] +
                uppercase[13:] + uppercase[:13]))

def rot13_2(message: str) -> str:
    """Implement a Caesar Cipher by shifting letters 13 places

    Non-english letters are left as-is

    Args:
     message: string to encode

    Return:
     the encoded version of the input string
    """
    return "".join(CODE.get(letter, letter) for letter in message)
tester(rot13_2)

This is more compact, although I'm not sure that the slicing is as immediately obvious as the use of the modulo is.

Using translate and maketrans

Here's a version using the built-in maketrans and translate functions.

CODE = str.maketrans(lowercase + uppercase,
                     lowercase[13:] + lowercase[:13] +
                     uppercase[13:] + uppercase[:13])


def rot13_3(message: str) -> str:
    """Implement a Caesar Cipher by shifting letters 13 places

    Non-english letters are left as-is

    Args:
     message: string to encode

    Return:
     the encoded version of the input string
    """
    return message.translate(CODE)
tester(rot13_3)
coded = rot13_3("I have been to paradise 3 times, but I have never been to me. "
                "Oh, the humanity!")
print(coded)
print(rot13_3(coded))
V unir orra gb cnenqvfr 3 gvzrf, ohg V unir arire orra gb zr. Bu, gur uhznavgl!
I have been to paradise 3 times, but I have never been to me. Oh, the humanity!

It kind of seems too much to use translate for this exercise, but it does feel cleaner than the dictionary, so I'll have to keep it in mind for the future.

CodeWars: Calculating With Functions

The Problem

Write functions that calculate integer arithmetic. For example.

seven(times(five()))

Should return thirty-five. Every number has a function and there are four operation-functions:

  • plus
  • minus
  • times
  • divided_by

All operations should return integers, not floats.

The Solution

# python
from functools import partial

def digit(operation=None, integer=None):
    """A base function to define a digit

    Args:
     operation: a function that expects an integer argument when called
     integer: an integer to return if no operation is passed in
    """
    if operation is not None:
        return operation(integer)
    return integer

# the digits
zero = partial(digit, integer=0)
one = partial(digit, integer=1)
two = partial(digit, integer=2)
three = partial(digit, integer=3)
four = partial(digit, integer=4)
five = partial(digit, integer=5)
six = partial(digit, integer=6)
seven = partial(digit, integer=7)
eight = partial(digit, integer=8)
nine = partial(digit, integer=9)

# the operations
def plus(right: int):
    return lambda left: left + right

def minus(right: int):
    return lambda left: left - right

def times(right: int):
    return lambda left: left * right

def divided_by(right):
    return lambda left: left // right

The Tests

expect(seven(times(five()))).to(equal(35))
expect(four(plus(nine()))).to(equal(13))
expect(eight(minus(three()))).to(equal(5))
expect(six(divided_by(two()))).to(equal(3))

Alternatives

There are several variations on the theme. One that I thought was similar in spirit to what I did but better was this one. Instead of separate operation and integer they use a default function that only returns what gets passed to it. So the definitions look like this.

def identity(integer: int) -> int:
    return integer

def zero(f=identity):
    return f(0)

def one(f=identity):
    return f(1)

def two(f=identity):
    return f(2)

def three(f=identity):
    return f(3)

def four(f=identity):
    return f(4)

def five(f=identity):
    return f(5)

def six(f=identity):
    return f(6)

def seven(f=identity):
    return f(7)

def eight(f=identity):
    return f(8)

def nine(f=identity):
    return f(9)

expect(seven(times(five()))).to(equal(35))
expect(four(plus(nine()))).to(equal(13))
expect(eight(minus(three()))).to(equal(5))
expect(six(divided_by(two()))).to(equal(3))

A Hybrid

To add a little of what the other solution is doing…

# python
from functools import partial

def identity(integer: int) -> int:
    """A pass-through function

    Args:
     integer: a digit input

    Returns:
     the integer given
    """
    return integer

def digit(operation=identity, integer=None):
    """A base function to define a digit

    Args:
     operation: a function that expects an integer argument when called
     integer: an integer to return if no operation is passed in
    """
    return operation(integer)

# the digits
zero = partial(digit, integer=0)
one = partial(digit, integer=1)
two = partial(digit, integer=2)
three = partial(digit, integer=3)
four = partial(digit, integer=4)
five = partial(digit, integer=5)
six = partial(digit, integer=6)
seven = partial(digit, integer=7)
eight = partial(digit, integer=8)
nine = partial(digit, integer=9)

# the operations
# this is a style some people used. I'm not sure I like it.
plus = lambda right: lambda left: left + right
minus = lambda right: lambda left: left - right

# alternatively you could just do this
def times(right: int): return lambda left: left * right
def divided_by(right): return lambda left: left // right

expect(seven(times(five()))).to(equal(35))
expect(four(plus(nine()))).to(equal(13))
expect(eight(minus(three()))).to(equal(5))
expect(six(divided_by(two()))).to(equal(3))

Coding Train Starfield

The Starfield

This is another p5.js hello-world, this time taken from Daniel Schiffman's Starfield in Processing coding challenge. It's a rough version of traveling through the stars at warp speed. He managed to do it in about 15 minutes, if I remember correctly. It starts static but if you move your mouse back and forth horizontally it adjusts the speed.

The Main Sketch

The most basic processing/p5 sketch uses two functions setup to initially set up your canvas and draw to update the frames over time. This function creates our canvas and star objects in the setup and then calculates a speed based on the mouse position of the user in order to update the stars. It gets passed a p5 element, called p, in order to get access to the p5.js code.

/** The main sketch
 * this gets passed to p5 so it defines the setup and draw functions
 * that p5 expects
*/
let starfield_sketch = function(p) {
  let star_count = 800;
  let parent_div_id = "schiffman-starfield";
  p.BLACK = 0;
  p.WHITE = 255;
  p.ALPHA = 100;

The Setup Function

Not too much voodoo here, other than the use of the JQuery outerWidth method which gets the width of the DIV that we're using to hold the sketch and uses it as the width for the canvas.

p.setup = function() {
  p.stars = [];
  this.canvas = p.createCanvas($("#" + parent_div_id).outerWidth(true), 800);
  for (let i=0; i < star_count; i++) {
    p.stars[i] = new Star(p);
  }
} // end setup

Draw

Again, not too fancy. The draw function:

  • Sets the background to black to wipe out our previous drawing
  • gets the "speed" of the stars using the x-position of the mouse and mapping this location from 0 to the width of the canvas to a smaller range from 0 to 50
  • Translate the (0, 0) of the coordinate system from the top left of the canvas to the middle so our stars emerge from the center instead of the top left
  • update all the stars with the speed and re-draw them
  p.draw = function() {
    p.background(p.BLACK, p.ALPHA);
    speed = p.map(p.mouseX, 0, p.width, 0, 50);
    p.translate(p.width/2, p.height/2);
    for (let i=0; i < p.stars.length; i++){
      p.stars[i].update(speed);
      p.stars[i].show();
    }
  } //end draw
}; //end starfield_sketch

The Star Class

The Star stores the position of a "star" and updates it based on the speed that it's given. Our initial constructor sets up the coordinates of the star using random values.

function Star(p) {
  this.x = p.random(-p.width, p.width);
  this.y = p.random(-p.height, p.height);
  this.z = p.random(p.width);

The Update

Most of the time the update reduces the z value by the current speed, but since we don't want the stars to go off the canvas and disappear, if it gets too small we re-randomize the position of the star.

this.update = function(speed) {
  this.z = this.z - speed

  if (this.z < 1) {
    this.x = p.random(-p.width, p.width);
    this.y = p.random(-p.height, p.height);
    this.z = p.random(p.width);
  }
} //end update

The Show Function

The show function is where most of the work is done. It calculates the proprotions of x and y to z and then maps that to the width and height of the canvas and then draws an ellipse. To get the radius of the ellipse we map the current z-value using an inverted target of 16, 0. This means that as z gets smaller our radius gets bigger.

  this.show = function() {
    p.fill(p.WHITE);
    var x_now = p.map(this.x/this.z, 0, 1, 0, p.width);
    var y_now = p.map(this.y/this.z, 0, 1, 0, p.height);

    var radius = p.map(this.z, 0, p.width, 16, 0);
    p.ellipse(x_now, y_now, radius, radius);

    p.stroke(p.WHITE);
  } // end show
}; //end class Star

Attaching the Sketch

This next bit attaches our sketch to a specific DIV defined in the HTML. You don't have to do this, you could just use the parts as universal functions like the examples show, but if you have more than one sketch on a page sometimes things get funky so I prefer this pattern to keep everything in place.

// Attach the starfield_sketch function at the top to the div with ID
// schiffman-starfield
sketch_container = new p5(starfield_sketch, 'schiffman-starfield');

Source

CodeWars: Vowel Count

The Problem

Given a string, count the number of vowels in it. The vowels are "aeiou" and the letters will be lower-cased.

The Solution

The Tests

# pypi
from expects import equal, expect

expect(vowel_count("a")).to(equal(1))
expect(vowel_count("rmnl")).to(equal(0))
expect(vowel_count("a mouse is not a house")).to(equal(10))

The Function

VOWELS = set("aeiou")

def vowel_count(letters: str) -> int:
    """Counts the number of vowels in the input

    Args:
     letters: lower-cased string to check for vowels

    Returns:
     count of vowels in the letters
    """
    return sum(1 for letter in letters if letter in VOWELS)

Alternatives

One solution used regular expressions and the findall method. This seems better in a generalizable sense, but I think that the findall will build a list rather than a generator so might not be as efficient space-wise, and is probably slower. Others used the python string method - count. I think this problem is so easy that there's really not a lot of stuff you can do that doesn't overcomplicate things.

Anyway, day one.

End

Mozilla Madness: Resist Fingerprinting!

The Short Version For My Future Self

Although some sites tell you to set Firefox's privacy.resistFingerprinting option to true, it breaks altair's interaction and some other sites that use the canvas. It's probably better not to use that option, but if you do either:

  • Install the Toggle Resist Fingerprinting extension and turn it off when things break (or just keep turning it off in about:config).
  • Or set privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts to False and accept the popup requests for the page you want to use.

Back To the Story: It Was a Dark and Rainy Day

I decided to give altair, the python data visualization library a try yesterday, just to see what it looked like. I ran their "hello, world" example and managed to get a plot.

Figure Missing

Nothing fancy. If you move your cursor over the bars you might get a tool-tip giving you the width of the bar. If you do then you don't have the problem I ran into yesterday when I was trying it out. The image itself came out clear enough, but I couldn't figure out how to make the tool-tips work. I got desperate enough to try and read the documentation but it seems to be split between examples and API descriptions with little more in the way of explanatory documentation that would help to figure out how it was supposed to work. There are a lot of examples, though, so I decided to see if they would help, but then when I was looking at their Scatter Plot with Tool-tips example I noticed that their plots didn't have tool-tips either, which seemed suspicious. Was their library that broken? Was the internet?

I took a look at the JavaScript console and that's when I saw these messages.

nil

It looked like it might be important, but when I went searching for the message I couldn't find anything relevant. At least not at first.

Start With the Nuclear Option

My first thought was that they had somehow made altair Chromium-only so I installed brave and, sure enough, the tool-tips worked when I switched browsers. So my initial conclusion was that I'd have to switch browsers if I decided to use altair. But then it occurred to me that I'd had problems in the past with some sites and anti-tracking options turned on in firefox so maybe it had something to do with that or one of the extensions. The question was, what setting was it or what extension? I eventually decided it was too much work to figure out so I first tried to use Troubleshoot Mode to disable all the extensions, and when that didn't work, I did a refresh and wiped out all the customizations I'd done to firefox. Amazingly, this worked, but now I had to go about setting Firefox up again while avoiding whatever I did that broke altair.

The Slow Crawl Back

I decided to follow the advice on the restoreprivacy.com page, just because it came up on the first page of my search results and it seemed to touch most of the bases that I'd run before this episode. As I did a step in the setup I would check back with the altair plot to make sure that the tool-tip was still working until, eventually, I came to the setting that broke it - privacy.resistFingerprinting.

nil

When I set it to true the tool-tips would break and when I set it to false they would work again. So, then what? I didn't want to disable fingerprint protection, so I thought I'd do a little more searching and see if there was another way.

Is It a Bug?

I decided to do a search on Bugzilla and found what seemed like a relevant bug: WhatsApp Web images broken if you flip `privacy.resistFingerprinting` due to canvas prompts without user interaction. The discussion is about What's App and also mentions Instagram and Twitter, and although they are focused on images, the actual error seemed close enough to what I was seeing that it seemed like it might be the same or a similar thing. But the bug was opened two years ago, so if it is a bug it doesn't seem to be something they're eager to fix. Then I ran across this bug: Do not display Canvas Prompt unless triggered by user input which discusses changing the default behavior to not prompt the user for permission to use the canvas. Now that I'm describing it I'm not sure how I came to this next step based on that bug, but for some reason I went looking in about:config again and noticed that right under resistFingerprinting was resistFingerprinting.autoDeclineNoUserInputCanvasPrompts:

nil

This option is set to True by default and seemed to be what they were talking about in the bug so I turned it off and went back to the altair page and this time when I put my cursor over the plot a popup came up asking me for permission.

nil

Once allowed it, it worked, even with resistFingerprinting set to true. So, if I understand what the bug reports were saying, what was causing the problem is that firefox is getting a request for the canvas but it decides that it isn't the user initiating it, so it needs extra permission but by default the box to ask for permission is turned off and the request is declined without the user (me) getting any feedback. This seemed like a bug.

As I was thinking about this I remembered that a couple of days ago I went to the kindle cloud reader and one of the books wouldn't render. I went back and played with turning resistFingerprinting on and off before trying to load the ebook and this was apparently the culprit so it isn't just altair that's affected for me.

So it's a bug, right? It seemed like one, but there were already reports of this phenomena for other sites going back years so they appear to have done it on purpose. I was trying to figure out whether it was something that should be reported or not when I came upon this bug: Users enable `privacy.resistFingerprinting` and then are surprised when it causes problems.

nil

Despite the fact that I've seen multiple sites saying to enable this "feature" maybe it isn't really a good idea after all. In the near-term I installed the Toggle Resist Fingerprinting extension and use it to turn resistFingerprinting on and off. To be honest, I'm not convinced that it really matters, I just got sucked into a trail of sites making conflicting assertions about what to do and convinced myself that I cared. At least I can read kindle books in the browser again.