Posts in Javascript (20 found)
Julia Evans Yesterday

Some more things about Django I've been enjoying

Hello! I’m on a funny journey right now where I’m trying to learn how to make websites in a sort of 2010 style, where I have an SQL database and render some HTML on the backend. It’s kind of an interesting journey because it doesn’t necessarily feel “easy” to me to make websites in this way: I never learned how to do it in the 2000s or 2010s, and there’s a lot I need to learn. So here are some Django features that make building this kind of site feel more achievable than when I was trying and failing to use Go’s standard library or Flask. And I’ll talk about a couple of issues with Django I’ve run into. Previously the toolkit I felt confident with for making websites was: I really liked this frontend-heavy approach for these super simple applications but when I started thinking about making something with a lot of different pages (instead of literally just one page), I didn’t feel so excited about the options I saw that involved a lot of frontend code. So I figured I’d try the backend. Writing a backend-focused site that uses as little JS as possible feels the same to me in a way as writing a single-page JS website that does as little on the backend as possible, even though they might seem like opposites. In both cases I’m just trying to keep as much of the logic as possible in one place. Now for some thoughts about Django! I learned that I can define a “query set” class in Django with a bunch of methods with different statements I might want to use while constructing a query: Here’s how I use it in my view code once I’ve defined what all the methods mean: and here’s how I define the methods: The syntax for defining the filters isn’t my favourite, but I spend most of my time just using the methods, and it feels super readable and nice to use, and it makes me want to look into other query builder libraries in the future. In the past I thought “I know SQL, who needs a query builder?”, but this kind of structure does make it really nice to read. I found an example of someone who wrote their own small query builder in Python that I want to read later to think about whether I would enjoy using a more minimal version of this. There are a bunch of little quality of life filters available in Django templates that are super useful for generating HTML. The ones I’ve used so far are: These are all small things individually but I feel like it makes a big difference somehow to just have them available. I think my favourite template filter is : in this site sometimes we use filters like to decide what’s displayed. that will make a link to the same query string with one change, like this to link to the previous date: Or to remove the parameter: I still really love Django’s automatic database system. It’s amazing to be able to just edit a model to add a new field or whatever, and then Django automatically generates the migration. So far we have done 19 database migrations and I think there will probably be more! It makes a huge difference for me to be able to just easily change the database as my understanding of the problem changes. Django’s documentation sometimes offers the option of using class-based views and inheritance to organize the code in your views. For example I have four views that share a lot of code, and I could use inheritance to manage that by defining some kind of parent class and then having my other views inherit from it. I tried it out and I did not enjoy the experience of using inheritance to share code between views. I switched to using functions instead, sort of how this post advocates, and that was a lot more straightforward. I’ve never had a good experience using inheritance in Python and I don’t think I’ll try to use it again. But I don’t mind using inheritance to use the interfaces Django itself provides: for example if I want to define a query set I need to write something like . I don’t think too hard about it and it seems to work. (as a meta comment: I’ve been working on talking about my programming opinions by just saying “THING does not feel good to me, I prefer OTHER THING instead”. That post I linked to says that function-based views are the “right way”. I’m not very invested in whether it’s “right”, but it’s validating to know that other people feel similarly to me about inheritance) At some point the LLM scrapers discovered our site, and started sending us maybe 10 requests per second. I blocked them which is working for now, but it made me think about what the site’s capacity is. I’m used to writing Go backends where the performance situation is pretty straightforward (usually everything is just fast enough), and a Django site is very different. Some light load testing (with ( ) shows that right now we can serve about 2-3 requests per second (on a ~$10/month VM). It’s tempting for me to go down a rabbit hole where I do a bunch of profiling to figure out what’s slow and try to make it faster (there’s py-spy for that, and py-spy is great and super easy to use, and profiling is fun!) But I really don’t understand what I should expect in terms of performance from a Django site and how I should be thinking about at a higher level. Some things I haven’t figured out yet: I think one thing I’m learning about Django is that because it’s a Framework (tm), it’s easy to accidentally misconfigure it. For example, when I was thinking about why my site was slow just now, I read the django performance docs and I noticed a comment saying: Enabling the cached template loader often improves performance drastically, as it avoids compiling each template every time it needs to be rendered. When I’d done CPU profiling I’d noticed that it was spending a lot of time rendering templates! Maybe this could help me! Clicking through the link, I saw that the cached template loader was supposed to be on by default, but I’d turned it off by accident while trying to do something else. I think this “I turned off the cached template loader by default” things is an example of how I still find the django settings file to be pretty confusing and difficult. I guess I should just be careful when I go in there. After turning on template caching, it seems like the site can now pretty easily handle 12 requests per second or so without using all of the CPU. I have not carefully benchmarked the before and after but it seems like it’s made a pretty big difference. One thing that’s been surprising to me about Django performance is that I’ve always heard the advice “if you have a performance problem, check your database queries! Maybe add an index!”. But I’ve been running into a variety of performance issues (like this template caching thing) that are not because of slow queries, so instead it’s been more useful for me so far to start by running a CPU profile. And since I’m using SQLite, any slow database query problem will show up on the CPU profile anyway. Anyway I don’t want to get too far into site performance. Like I said it’s easy for me to get interested in profiling, but actually I know a lot about profiling and it’s not the most important thing for me to learn about. I might say more about what I’m enjoying (or having a hard time with!) about Django later. Trying to write some shorter blog posts recently. static site generators (like for this blog) static sites that do some fun stuff with Javascript (like this sql playground ) simple Vue.js single page apps with either a Lambda as a backend or a Go backend (like mess with dns ) translating plain text URLs into links, or line breaks into ( ) formatting dates ( ) , which takes a Python dictionary and automatically converts it to JSON and inserts it into the HTML as a tag in a safe way If I have a site that’s going to be getting occasional bursts of traffic, do I want to be able to scale up? Do I want to design the site so that more things can be cached? (and do I really have to? caches are so annoying to get right!) The django performance docs say that Jinja is faster for templating, do I want to think about switching templating systems? Those docs also say “{% block %} is faster than using {% include %}”, I wonder if it’s a big difference and if so why

0 views
Maurycy 5 days ago

Regressive JPEGs:

One of the cool features of JPEG files is that there's the option to save low frequency components first. This means that a partially downloaded image will be displayed at low resolution instead of being cut off. In the file, this works by breaking up the compressed data into multiple "scans", each prefixed with a header. Here's the first scan of a representive image: ... this one includes the lowest (DC) Fourier bin for all three color channels. The three color channels are YCbCr instead of the usual RGB. The luminance (Y) seperated because it must be high quality, but the color can be fudged quite a bit while looking fine. Very roughly: Y = G, Cb = B - G, Cr = R - G After it, the file contains eight more scans to fill in the rest of the data: Scan number Channels DCT bin range Precision 0 Y Cb Cr 0 - 0 Half (-1 bit) 1 Y 1 - 5 Quarter (-2 bits) 2 Cb 1 - 63 Half 3 Cr 1 - 63 Half 4 Y 6 - 63 Quarter 5 Y 1 - 63 Half 6 Y Cr Cb 0 - 0 Full 7 Cr 1 - 63 Full 8 Cb 1 - 63 Full 9 Y 1 - 63 Full Scan #0 contains a very low resolution preview of the image. Scan #1 adds some details to the luminance. Scans number two through five contain full low precision data. Scan 4 has an unusual spectral range because it's filling in the gap left by #1. That way, number 5 has full quarter precision data to build on. Scans six through nine add the final missing bit to bring the image to full quality. Given what I said about color being less important, it might seem weird that my example has the color data first: This works because the the chrominance is saved at half resolution (quarter pixel count). As a result, full chrominance data (Cr + Cb) only weighs half as much as luminance. Since each scan explicitly sets its spectral range , it should be possible to construct a JPEG file where future scans overwrite already rendered image data. Actually, it's very easy to do this: Concatenate multiple images with the same resolution and filter out the start-of-image, start-of-frame and end-of-image markers. This can be done in a hex editor, but I used a quick and dirty C program. When served over a slow network , this concatenated file will switch between multiple images: Click to open in new tab But, most decoders will give up after some number of scans : I think this is done to avoid a zip bomb style problem... but it prevents this from working on more than 9 frames, which is not enough for a proper animation. To do that, I'd have to minimize the number of scans in each frame. The simplest idea is to start with baseline JPEGs that only have a single scan. ... but it doesn't work: In progressive mode, a scan can't contain both AC (bins above 0) and DC (bin 0) data at the same time. This limitation doesn't exist for baseline mode, but the baseline decoder stops after the first scan. Since AC data must follow DC data, the smallest possible "progressive" JPEG contains a single DC-only scan. Because the DCT runs on 16x16 blocks, such an image won't a solid color: it'll be 1/16th of the original resolution. Scan number Channels DCT bins Precision 0 Y Cb Cr 0 - 0 Full Doing this, I can get Chrome to render around 90 frames before giving up. Other browsers like Firefox have more patience, but a 90 scan image seems to work almost everywhere. As a bonus, this avoids the ghosting of the naive attempt: that happened because AC scans are supposed to refine old data. Normally, this allows images to include multiple precision levels without inflating file size... but doesn't play nicely with my tricks. If the file only includes DC scans with no actual progression, this isn't a problem. Since a "DC-only" frame is a standards-compliant images , creating them doesn't require anything special: Using these, it's possible to pack a whole video inside a single image: Click to open in new tab Besides unconventional rickrolls and other trolling, this has no practical applications: there's no way to add timing information, so playback is entirely dependent on network delay. ... although there is a lot of fun to be had using partial rendering: This is a pure HTML video using <dialog> tags: badapple.rose.systems Of course, there's no rule that the data must be hardcoded: here's a interactive single-page application with no CSS or JavaScript. (seems slighty broken, I'll investigate later) Related : /projects/bad_jpeg/merge.c : The code used to generate these images /projects/bad_jpeg/merge.c : The code used to generate these images

0 views

Notes on the Fourier Transform

The Fourier series is a great tool for analyzing periodic functions. But what about functions that don’t repeat? We’ve seen that we can compute Fourier series for a non-periodic function defined on a finite interval, as long as we don’t care about its behavior beyond that interval. Let’s extend this idea to functions that never repeat; that is, non-periodic functions defined on the interval (-\infty,\infty) . To motivate the subject ahead, let’s look back at the example used in the earlier post about Fourier series : With an odd extension into [-2,0] . In that post, to make the Fourier series work, we assumed t(x) keeps repeating with a period 2L=4 on the entire x axis. Here, let’s face the reality that it does not - in fact - repeat, and observe how our Fourier series work out. Recall that the Fourier series approximating t(x) are the sine series (since it’s an odd function): The following visualization is interactive. By default, it shows t(x) (with its odd extension) and no Fourier series approximation. We’ll proceed by a series of steps and observe the outcome: Step 1 : set to some non-zero number; already at 3, the approximation is very good. The frequency spacing is \frac{\pi}{L} (this is the coefficient of x in the sines). Note that the Fourier series repeats every 2L , as expected. Step 2 : increase L to 6. This means our series are constructed assuming t(x) has a period of 12, not 4. Note how the Fourier series look now - they repeat every 12, and they don’t match t(x) as well as before. We can increase to a higher number to make the match better. As L grows, the spacing between adjacent frequencies decreases. Step 3 : increase L to 10. We no longer see the repetitions, so feel free to increase the values of x min and x max until you do. Note again that we need to add more and more coefficients to match t(x) better with this larger L , and the spacing adjacent frequencies grows smaller. Increasing L means our function repeats at larger and larger intervals. The logical conclusion of this progression is to ask - what happens if the function never repeats, meaning L\rightarrow\infty ? While not mathematically rigorous, the visual experiment here lets us make some conjectures: we’ll likely need an infinite number of coefficients for a good approximation, and moreover, the spacing between these coefficients will tend to zero. In other words, instead of a discrete set of coefficients, we’ll end up with a continuous line, or function . The function produced by this process is the Fourier transform of t(x) , and the next section shows its mathematical derivation. In these notes, we’ll be using the complex exponential formulation of Fourier series: We’re interested in a non-periodic defined on the interval (-\infty,\infty) . So we’ll be exploring the above equations for L\rightarrow\infty . First, let’s make a slight change of notation. Instead of writing formulae in terms of the period ( 2L ), we’ll be using the n-th harmonic angular frequency w_n : So we can slightly rewrite our series as: Using \Delta w as the difference between two consecutive frequencies: Using this notation, C_n is expressed as: So far there are no new insights here, just some new notation. Now we’re going to use it to facilitate the next step. Since L\rightarrow \infty , then \Delta w\rightarrow 0 . Let’s calculate the limit of the Fourier series representation of when \Delta w\rightarrow 0 : And substitute the latest C_n into this equation, changing its dummy integration variable from x to t to avoid confusion [1] Reordering slightly, and also replacing n\Delta w by w_n in the complex exponents: Looking at the limit with the sum carefully, this is a Riemann sum (see Appendix A)! w_n is the "sampled" version of , and \Delta w\rightarrow 0 . We can therefore replace it by an integral, changing w_n to and \Delta w to dw [2] : The inner integral is called the Fourier transform of and denoted [3] : And the full equation for is then the inverse Fourier transform: Let’s take our favorite odd triangular pulse example and calculate its Fourier transform. The function’s mathematical definition and plot are shown earlier in this post. Note that we’re not extending this function periodically - it’s zero beyond the range [-2,2] ; this is exactly why we need the Fourier transform here - as we’ve seen, Fourier series won’t do because the function they reconstruct eventually starts repeating. We’re looking to find: To calculate the integral, let’s decompose the complex exponent using Euler’s formula: Since our t(x) is odd, the first integral is zero . Also t(x)sin(wx) is even, so we can write: We’ve already calculated a very similar integral in the post on Fourier series , so let’s just skip to the result: The only remaining difficulty is its value at 0, which seems undefined at first (division by zero). However, note that as w\rightarrow 0 , the numerator also tends to 0, so we can use L’Hopital’s rule (twice!) to find that: This function is complex-valued; in fact, it’s purely imaginary. How do we visualize it? A common way to visualize complex-valued functions is by plotting their magnitude and phase separately. The magnitude of \hat{t}(w) is: Since \hat{t}(w) is purely imaginary, there are only two options for the phase: When the numerator is positive, we get a negative imaginary number with phase -\pi/2 , and when the numerator is negative, we get a positive imaginary number with phase \pi/2 . Finally, when \hat{t}(w)=0 (which happens at w=0 , by our earlier analysis, but also whenever is a whole multiple of \pi ), the phase is undefined. Here’s the magnitude and phase of \hat{t}(w) plotted against : It is common to talk about \hat{t}(w) as the frequency domain representation of t(x) . When the functions we’re working with have time as their domain (e.g. the x in t(x) represents time), which is often the case in the study of signals and systems, the Fourier transform can be seen as computing the frequency domain representation of the function. Here’s the Fourier transform formula again: It takes - the time domain representation of a function, and converts it to \hat{f}(w) - a frequency domain representation. For well-behaved functions, these two representations are dual - each one describes the function completely, just in a different way. To convert back from a frequency domain representation to the time domain, we use the inverse Fourier transform: While a time-domain plot ( t(x) ) shows how a signal changes over time, a frequency-domain plot ( \hat{t}(w) ) shows how the signal is distributed across all possible frequencies. Moreover, as we’ve seen, \hat{t}(w) is complex valued. Each frequency therefore has both a magnitude and a phase: the magnitude tells us how strongly that frequency contributes, while the phase tells us how that component is shifted. The frequency domain is extremely useful in signal analysis; for example, when designing filters. The Fourier transform also has a number of properties that are very useful in signal analysis and processing. But first, let’s discuss what a "well-behaved function" means for the purpose of applying Fourier transforms. The simplest existence condition for Fourier transforms is absolute integrability (also known as Lebesgue integrable): With this condition, \hat{f}(w) exists on the entire domain, is continuous and vanishes (tends to 0) as |w|\rightarrow\infty [4] . While this condition is sufficient, it’s not necessary; there are less well-behaved functions that also have Fourier transforms defined with some limitations. In these notes, we’re mostly interested in well-behaved functions that are used in real-world engineering, so we won’t discuss the other cases. Another assumption commonly made for real-world functions is that they vanish (tend to 0) as |x|\rightarrow\infty . While this is not a direct outcome of absolute integrability [5] , it’s a reasonable assumption in engineering. After all, real-world signals have finite energies. Intuitively, when we also assume is uniformly continuous , the assumption of vanishing at |x|\rightarrow\infty is a logical conclusion, because otherwise how can the total area for |f(x)| be finite? An important outcome of this discussion is that the Fourier transform is unsuitable for periodic functions. Functions that repeat at intervals are not absolute integrable . For periodic functions, we use Fourier series. The Fourier transform is a linear operator, because the integral is linear: So is the inverse Fourier transform; it’s similarly easy to show that: If we scale the domain of a function by a constant, its transform changes only slightly: Let’s do the variable substitution u=ax : This is the Fourier transform evaluated at \frac{w}{a} , so: There’s one small caveat here; when a is negative, the integral bounds should be flipped, causing a minus sign in front of the transform. So we can write: Which works for any a\ne 0 . This property is intuitive when thinking about signals: suppose a>0 , then f(ax) means the signal is compressed in the time domain by a factor a . The scaling property says that the frequency domain is expanded using the same factor; in other words, the higher frequencies become more prominent because we need sharper transitions to represent the compressed signal. Time shifting What happens to the Fourier transform if we time-shift the input signal by some constant: f(x-x_0) . By definition: Substituting u=x-x_0 , we get du=dx , so: Transform of a derivative An extremely useful property that’s often employed in the solution of partial differential equations; let’s calculate the Fourier transform of the derivative of : We’ll use integration by parts, where dv=f'(x) and u=e^{-i\cdot wx} . Therefore, v=f(x) and du=-iw\cdot e^{-i\cdot wx} : Recall the assumption made in the "Existence condition..." section about vanishing at infinities. So the first part of the equation above is zero, and we’re left with: Transform of convolution The convolution between two continuous functions and g(x) is defined as: Let’s calculate the Fourier transform of this function: This step of combining the integrals into a double integral, as well as the next step (changing the order of integration) is possible due to Fubini’s theorem and our assumption that and g(x) are Lebesgue integrable. Switch order of integration: Now, f(\xi) in the inner integral doesn’t depend on x , so we can pull it out: The inner integral is just the Fourier transform of a time-shifted g(x-\xi) , so we can write: And the remaining integral is the Fourier transform of , so: Convolution in the time domain translates to multiplication in the frequency domain! This result is so important in signal processing that it’s called the convolution theorem . Suppose we have some function and we want to know the area bounded between this function’s graph and the x axis in a certain interval [a,b] . One way to do this is to take a partition of the interval: And calculate the area under for every element of the partition. We can then approximate such sub-areas by rectangles, as follows: We’ll denote the area of each rectangle as f(x^*_i)\cdot\Delta x : There are many ways to choose which point of the interval [x_{i-1},x_i] to denote as x^*_i : left point ( x_{i-1} ), right point ( ), mid-point between the two (which is what our plot shows) or anything in between. The distinction doesn’t really matter for our purpose, as we will soon see. We can approximate the area under the curve of in the interval [a,b] with the Riemann sum , using a uniform partition: If is continuous on [a,b] , then as n\rightarrow \infty : This is known as the Riemann integral , or just the definite integral. The limit is why the exact choice of x^*_i doesn’t matter: as n\rightarrow\infty we have \Delta x\rightarrow 0 , and all points within [x_{i-1}, x_i] are equally good. The Fourier series is a great tool for analyzing periodic functions. But what about functions that don’t repeat? We’ve seen that we can compute Fourier series for a non-periodic function defined on a finite interval, as long as we don’t care about its behavior beyond that interval. Let’s extend this idea to functions that never repeat; that is, non-periodic functions defined on the interval (-\infty,\infty) . Visualizing Fourier series for non-repeating functions To motivate the subject ahead, let’s look back at the example used in the earlier post about Fourier series : \[t(x)= \begin{cases} x & 0 \leq x \leq 1 \\ 2-x & 1 < x \leq 2 \\ \end{cases}\] With an odd extension into [-2,0] . In that post, to make the Fourier series work, we assumed t(x) keeps repeating with a period 2L=4 on the entire x axis. Here, let’s face the reality that it does not - in fact - repeat, and observe how our Fourier series work out. Recall that the Fourier series approximating t(x) are the sine series (since it’s an odd function): \[t(x)=\frac{8}{\pi^2}\bigg[ sin\frac{\pi x}{2}-\frac{1}{3^2} sin\frac{3\pi x}{2}+\frac{1}{5^2}sin\frac{5\pi x}{2}-\cdots\bigg]\] The following visualization is interactive. By default, it shows t(x) (with its odd extension) and no Fourier series approximation. We’ll proceed by a series of steps and observe the outcome: n (terms in the Fourier series) L x min x max Step 1 : set to some non-zero number; already at 3, the approximation is very good. The frequency spacing is \frac{\pi}{L} (this is the coefficient of x in the sines). Note that the Fourier series repeats every 2L , as expected. Step 2 : increase L to 6. This means our series are constructed assuming t(x) has a period of 12, not 4. Note how the Fourier series look now - they repeat every 12, and they don’t match t(x) as well as before. We can increase to a higher number to make the match better. As L grows, the spacing between adjacent frequencies decreases. Step 3 : increase L to 10. We no longer see the repetitions, so feel free to increase the values of x min and x max until you do. Note again that we need to add more and more coefficients to match t(x) better with this larger L , and the spacing adjacent frequencies grows smaller. Increasing L means our function repeats at larger and larger intervals. The logical conclusion of this progression is to ask - what happens if the function never repeats, meaning L\rightarrow\infty ? While not mathematically rigorous, the visual experiment here lets us make some conjectures: we’ll likely need an infinite number of coefficients for a good approximation, and moreover, the spacing between these coefficients will tend to zero. In other words, instead of a discrete set of coefficients, we’ll end up with a continuous line, or function . The function produced by this process is the Fourier transform of t(x) , and the next section shows its mathematical derivation. Fourier series with L\rightarrow\infty leading to Fourier transform In these notes, we’ll be using the complex exponential formulation of Fourier series: \[f(x)=\sum_{n=-\infty}^{\infty}C_n\cdot e^{in\pi x/L}\] With: \[C_n=\frac{1}{2L}\int_{-L}^{L}f(x)e^{-in\pi x/L}dx\] We’re interested in a non-periodic defined on the interval (-\infty,\infty) . So we’ll be exploring the above equations for L\rightarrow\infty . First, let’s make a slight change of notation. Instead of writing formulae in terms of the period ( 2L ), we’ll be using the n-th harmonic angular frequency w_n : \[w_n=\frac{n\pi}{L}\] So we can slightly rewrite our series as: \[f(x)=\sum_{n=-\infty}^{\infty}C_n\cdot e^{i w_n x}=\sum_{n=-\infty}^{\infty}C_n\cdot e^{i\cdot n \Delta w x}\] Using \Delta w as the difference between two consecutive frequencies: \[\Delta w=w_n-w_{n-1}=\frac{n\pi}{L}-\frac{(n-1)\pi}{L}=\frac{\pi}{L}\] Using this notation, C_n is expressed as: \[C_n=\frac{\Delta w}{2\pi}\int_{-\pi/\Delta w}^{\pi/\Delta w}f(x)e^{-i\cdot n \Delta w x}dx\] So far there are no new insights here, just some new notation. Now we’re going to use it to facilitate the next step. Since L\rightarrow \infty , then \Delta w\rightarrow 0 . Let’s calculate the limit of the Fourier series representation of when \Delta w\rightarrow 0 : \[f(x)=\lim_{\Delta w\rightarrow 0}\sum_{n=-\infty}^{\infty}C_n\cdot e^{i\cdot n \Delta w x}\] And substitute the latest C_n into this equation, changing its dummy integration variable from x to t to avoid confusion [1] \[f(x)=\lim_{\Delta w\rightarrow 0}\sum_{n=-\infty}^{\infty}\left[\frac{\Delta w}{2\pi}\int_{-\pi/\Delta w}^{\pi/\Delta w}f(t)e^{-i\cdot n \Delta w t}dt\right]\cdot e^{i\cdot n \Delta w x}\] Reordering slightly, and also replacing n\Delta w by w_n in the complex exponents: \[f(x)=\frac{1}{2\pi}\lim_{\Delta w\rightarrow 0}\sum_{n=-\infty}^{\infty}\left[\int_{-\pi/\Delta w}^{\pi/\Delta w}f(t)e^{-i\cdot w_n t}dt\right]\cdot e^{i\cdot w_n x}\Delta w\] Looking at the limit with the sum carefully, this is a Riemann sum (see Appendix A)! w_n is the "sampled" version of , and \Delta w\rightarrow 0 . We can therefore replace it by an integral, changing w_n to and \Delta w to dw [2] : \[f(x)=\frac{1}{2\pi}\int_{-\infty}^{\infty}\left[\int_{-\infty}^{\infty}f(t)e^{-i\cdot wt}dt\right]\cdot e^{i\cdot w x}dw\] The inner integral is called the Fourier transform of and denoted [3] : \[\boxed{\hat{f}(w)=\mathcal{F}\left[f(x)\right]=\int_{-\infty}^{\infty}f(x)e^{-i\cdot wx}dx}\] And the full equation for is then the inverse Fourier transform: \[\boxed{f(x)=\mathcal{F}^{-1}\left[\hat{f}(w)\right]=\frac{1}{2\pi}\int_{-\infty}^{\infty}\hat{f}(w)e^{i\cdot w x}dw}\] Example calculation of Fourier transform Let’s take our favorite odd triangular pulse example and calculate its Fourier transform. The function’s mathematical definition and plot are shown earlier in this post. Note that we’re not extending this function periodically - it’s zero beyond the range [-2,2] ; this is exactly why we need the Fourier transform here - as we’ve seen, Fourier series won’t do because the function they reconstruct eventually starts repeating. We’re looking to find: \[\hat{t}(w)=\int_{-\infty}^{\infty}t(x)e^{-iwx}dx\] To calculate the integral, let’s decompose the complex exponent using Euler’s formula: \[\hat{t}(w)=\int_{-\infty}^{\infty}t(x)cos(wx)dx-i\int_{-\infty}^{\infty}t(x)sin(wx)dx\] Since our t(x) is odd, the first integral is zero . Also t(x)sin(wx) is even, so we can write: \[\hat{t}(w)=-2i\int_{0}^{\infty}t(x)sin(wx)dx\] We’ve already calculated a very similar integral in the post on Fourier series , so let’s just skip to the result: \[\hat{t}(w)=-2i\cdot\frac{2\cdot sin(w)-sin(2w)}{w^2}\] The only remaining difficulty is its value at 0, which seems undefined at first (division by zero). However, note that as w\rightarrow 0 , the numerator also tends to 0, so we can use L’Hopital’s rule (twice!) to find that: \[\lim_{w\rightarrow 0} \hat{t}(w)=0\] Therefore: \[\hat{t}(w)= \begin{cases} -2i\cdot\frac{2\cdot sin(w)-sin(2w)}{w^2} & w\neq 0 \\ 0 & w=0 \\ \end{cases}\] This function is complex-valued; in fact, it’s purely imaginary. How do we visualize it? A common way to visualize complex-valued functions is by plotting their magnitude and phase separately. The magnitude of \hat{t}(w) is: \[|\hat{t}(w)|=\sqrt{\hat{t}(w)\cdot\hat{t}(w)^*}=2\left|\frac{2\cdot sin(w)-sin(2w)}{w^2} \right|\] Since \hat{t}(w) is purely imaginary, there are only two options for the phase: When the numerator is positive, we get a negative imaginary number with phase -\pi/2 , and when the numerator is negative, we get a positive imaginary number with phase \pi/2 . Finally, when \hat{t}(w)=0 (which happens at w=0 , by our earlier analysis, but also whenever is a whole multiple of \pi ), the phase is undefined. Here’s the magnitude and phase of \hat{t}(w) plotted against : It is common to talk about \hat{t}(w) as the frequency domain representation of t(x) . The frequency domain representation of functions When the functions we’re working with have time as their domain (e.g. the x in t(x) represents time), which is often the case in the study of signals and systems, the Fourier transform can be seen as computing the frequency domain representation of the function. Here’s the Fourier transform formula again: \[\hat{f}(w)=\mathcal{F}\left[f(x)\right]=\int_{-\infty}^{\infty}f(x)e^{-i\cdot wx}dx\] It takes - the time domain representation of a function, and converts it to \hat{f}(w) - a frequency domain representation. For well-behaved functions, these two representations are dual - each one describes the function completely, just in a different way. To convert back from a frequency domain representation to the time domain, we use the inverse Fourier transform: \[\mathcal{F}^{-1}\left[\hat{f}(w)\right]=\frac{1}{2\pi}\int_{-\infty}^{\infty}\hat{f}(w)e^{i\cdot w x}dw\] While a time-domain plot ( t(x) ) shows how a signal changes over time, a frequency-domain plot ( \hat{t}(w) ) shows how the signal is distributed across all possible frequencies. Moreover, as we’ve seen, \hat{t}(w) is complex valued. Each frequency therefore has both a magnitude and a phase: the magnitude tells us how strongly that frequency contributes, while the phase tells us how that component is shifted. The frequency domain is extremely useful in signal analysis; for example, when designing filters. The Fourier transform also has a number of properties that are very useful in signal analysis and processing. But first, let’s discuss what a "well-behaved function" means for the purpose of applying Fourier transforms. Existence condition for the Fourier transform The simplest existence condition for Fourier transforms is absolute integrability (also known as Lebesgue integrable): \[\int_{-\infty}^{\infty}|f(x)|dx<\infty\] With this condition, \hat{f}(w) exists on the entire domain, is continuous and vanishes (tends to 0) as |w|\rightarrow\infty [4] . While this condition is sufficient, it’s not necessary; there are less well-behaved functions that also have Fourier transforms defined with some limitations. In these notes, we’re mostly interested in well-behaved functions that are used in real-world engineering, so we won’t discuss the other cases. Another assumption commonly made for real-world functions is that they vanish (tend to 0) as |x|\rightarrow\infty . While this is not a direct outcome of absolute integrability [5] , it’s a reasonable assumption in engineering. After all, real-world signals have finite energies. Intuitively, when we also assume is uniformly continuous , the assumption of vanishing at |x|\rightarrow\infty is a logical conclusion, because otherwise how can the total area for |f(x)| be finite? An important outcome of this discussion is that the Fourier transform is unsuitable for periodic functions. Functions that repeat at intervals are not absolute integrable . For periodic functions, we use Fourier series. Some useful properties of Fourier transforms Linearity The Fourier transform is a linear operator, because the integral is linear: \[\begin{aligned} \mathcal{F}\left[\alpha f(x)+\beta g(x)\right]&=\int_{-\infty}^{\infty}\alpha f(x)e^{-i\cdot wx}dx+\int_{-\infty}^{\infty}\beta g(x)e^{-i\cdot wx}dx\\ &=\alpha\int_{-\infty}^{\infty}f(x)e^{-i\cdot wx}dx+\beta\int_{-\infty}^{\infty}g(x)e^{-i\cdot wx}dx\\ &=\alpha\mathcal{F}\left[f(x)\right]+\beta\mathcal{F}\left[g(x)\right] \end{aligned}\] So is the inverse Fourier transform; it’s similarly easy to show that: \[\mathcal{F}^{-1}\left[\alpha\hat{f}(w)+\beta\hat{g}(w)\right]= \alpha\mathcal{F}^{-1}\left[\hat{f}(w)\right]+\beta\mathcal{F}^{-1}\left[\hat{g}(w)\right]\] Scaling If we scale the domain of a function by a constant, its transform changes only slightly: \[\mathcal{F}\left[f(ax)\right]=\int_{-\infty}^{\infty}f(ax)e^{-i\cdot wx}dx\] Let’s do the variable substitution u=ax : \[\mathcal{F}\left[f(ax)\right]=\frac{1}{a}\int_{-\infty}^{\infty}f(u)e^{-i\cdot \frac{wu}{a}}du\] This is the Fourier transform evaluated at \frac{w}{a} , so: \[\mathcal{F}\left[f(ax)\right]=\frac{1}{a}\hat{f}\left(\frac{w}{a}\right)\] There’s one small caveat here; when a is negative, the integral bounds should be flipped, causing a minus sign in front of the transform. So we can write: \[\mathcal{F}\left[f(ax)\right]=\frac{1}{|a|}\hat{f}\left(\frac{w}{a}\right)\] Which works for any a\ne 0 . This property is intuitive when thinking about signals: suppose a>0 , then f(ax) means the signal is compressed in the time domain by a factor a . The scaling property says that the frequency domain is expanded using the same factor; in other words, the higher frequencies become more prominent because we need sharper transitions to represent the compressed signal. Time shifting What happens to the Fourier transform if we time-shift the input signal by some constant: f(x-x_0) . By definition: \[\mathcal{F}\left[f(x-x_0)\right]=\int_{-\infty}^{\infty}f(x-x_0)e^{-i\cdot wx}dx\] Substituting u=x-x_0 , we get du=dx , so: \[\begin{aligned} \mathcal{F}\left[f(x-x_0)\right]&=\int_{-\infty}^{\infty}f(u)e^{-i\cdot w(u+x_0)}du\\ &=e^{-iwx_0}\int_{-\infty}^{\infty}f(u)e^{-i\cdot wu}du\\ &=e^{-iwx_0}\mathcal{F}\left[f(x)\right] \end{aligned}\] Transform of a derivative An extremely useful property that’s often employed in the solution of partial differential equations; let’s calculate the Fourier transform of the derivative of : \[\mathcal{F}\left[f'(x)\right]=\int_{-\infty}^{\infty}f'(x)e^{-i\cdot wx}dx\] We’ll use integration by parts, where dv=f'(x) and u=e^{-i\cdot wx} . Therefore, v=f(x) and du=-iw\cdot e^{-i\cdot wx} : \[\mathcal{F}\left[f'(x)\right]=\left[f(x)e^{-i\cdot wx}\right]^{\infty}_{-\infty}-\int_{-\infty}^{\infty}f(x)(-iw\cdot e^{-i\cdot wx})dx\] Recall the assumption made in the "Existence condition..." section about vanishing at infinities. So the first part of the equation above is zero, and we’re left with: \[\begin{aligned} \mathcal{F}\left[f'(x)\right]&=-\int_{-\infty}^{\infty}f(x)(-iw\cdot e^{-i\cdot wx})dx\\ &=iw\int_{-\infty}^{\infty}f(x)e^{-i\cdot wx}dx\\ &=iw\cdot\mathcal{F}\left[f(x)\right] \end{aligned}\] Transform of convolution The convolution between two continuous functions and g(x) is defined as: \[(f\ast g)(x)=\int_{-\infty}^{\infty}f(\xi)g(x-\xi)d\xi\] Let’s calculate the Fourier transform of this function: \[\begin{aligned} \mathcal{F}\left[(f\ast g)(x)\right]&=\int_{-\infty}^{\infty}e^{-i\cdot wx}\left[\int_{-\infty}^{\infty}f(\xi)g(x-\xi)d\xi\right]dx\\ &=\int_{-\infty}^{\infty}\int_{-\infty}^{\infty}e^{-i\cdot wx}f(\xi)g(x-\xi)d\xi\ dx \end{aligned}\] This step of combining the integrals into a double integral, as well as the next step (changing the order of integration) is possible due to Fubini’s theorem and our assumption that and g(x) are Lebesgue integrable. Switch order of integration: \[\mathcal{F}\left[(f\ast g)(x)\right]=\int_{-\infty}^{\infty}\int_{-\infty}^{\infty}e^{-i\cdot wx}f(\xi)g(x-\xi)dx\ d\xi\] Now, f(\xi) in the inner integral doesn’t depend on x , so we can pull it out: \[\mathcal{F}\left[(f\ast g)(x)\right]=\int_{-\infty}^{\infty}f(\xi)\int_{-\infty}^{\infty}e^{-i\cdot wx}g(x-\xi)dx\ d\xi\] The inner integral is just the Fourier transform of a time-shifted g(x-\xi) , so we can write: \[\mathcal{F}\left[(f\ast g)(x)\right]=\int_{-\infty}^{\infty}f(\xi)e^{-i\cdot w\xi}\mathcal{F}\left[g(x)\right]d\xi=\mathcal{F}\left[g(x)\right]\int_{-\infty}^{\infty}e^{-i\cdot w\xi}f(\xi)d\xi\] And the remaining integral is the Fourier transform of , so: \[\mathcal{F}\left[(f\ast g)(x)\right]=\mathcal{F}\left[f\right]\cdot\mathcal{F}\left[g\right]\] Convolution in the time domain translates to multiplication in the frequency domain! This result is so important in signal processing that it’s called the convolution theorem . Appendix A: Riemann sum and the definite integral Suppose we have some function and we want to know the area bounded between this function’s graph and the x axis in a certain interval [a,b] . One way to do this is to take a partition of the interval: \[a=x_0<x_1<\cdots<x_{n-1}<x_n=b\] And calculate the area under for every element of the partition. We can then approximate such sub-areas by rectangles, as follows: We’ll denote the area of each rectangle as f(x^*_i)\cdot\Delta x : \Delta x=(b-a)/n is the width of one interval (assuming a uniform partition, but the math works just as well for non-uniform ones). x^*_i is some value in the interval [x_{i-1},x_i] .

0 views
<antirez> 1 weeks ago

Control the ideas, not the code

Look at the past history of this blog. There are many blog posts about programming with AI, a few of them date back to January 2024 (like this: https://antirez.com/news/140). I’m a relatively well regarded programmer, after all. I don’t have the need to still be in the “loop” as a old man that seeks for relevance, I recently rejoined Redis, and now I also am developing a new open source software for local LLM inference that received a good welcome in the community. Why I keep doing this, of saying what people don’t want to hear? Why I keep announcing how future programming will be by default? Because I feel the urge of lowering the impact for people less prepared to the change than me, often younger than me, and that, unlikely me, didn’t see many of those things coming (In 2022 I published, before ChatGPT existed, a book preannouncing many things that now happened and other things that I believe *will* happen, so I feel like I can say this without sounding egocentric). So mine is a trick. People feel more and more programming is completely modified by AI and don’t know what they should do, if they can really start coding in a completely different way, without looking much at the code as their main output. They feel like they are betraying their own field. So my intention is to arrive and say “look at me, In can write code, you know, I’m not hiding behind AI: yet, things changed, it’s not your weakness, it’s not that you are AI-pilled. It is just that our field is evolving in an incredible *and* painful (but also joyful) direction”. This is why yesterday, on X, I said that I believe many programmers at this point have less impact they could have because they look at the code. I truly believe into that. And note that this does not mean to vibe code something just asking for the final product. The point is: if you control the ideas of your software, looking at the code itself is suboptimal and often pointless. For the following reasons: 1. You can now generate a lot of code, even *not* accounting for the LLM code verbosity (that is also effect of not being able to instruct them well, for most of the part). How are you supposed to review 5k lines of code every day? 2. LLMs are very good at writing locally optimal code, and are worse (but improving) with big ideas. What’s the point of scanning function by function, line by line? Instead you should prompt the design you have in mind, sometimes ask “how is exactly the design of that part? How does it work?”, and evaluate if it is the right model. It is much faster. 3. The working day is 8 hours. If you read the code, it is a tradeoff. You are doing less of what today is the most important part of your job, that is, asking yourself: what I’m doing with this software? What are the new directions I want to take? And also, think at new ideas, features, optimizations tricks. And doing a lot of QA. Controlling the ideas. Do you remember this phrasing from the Mythical Man Month? Well, a book from the 70s tells us more things about the current software era than many of the things that were said from 2000 to 2020. Why people that now protest against AI were not horrified by the state of software in the last decade? The level of slop we touched during recent years, before AI, is unbelievable. I’ll say you another thing. What is slop? With DwarfStar I implemented an inference for two LLMs (DeepSeek v4 and GLM 5.2) in a completely automated way, but: try it yourself, you will discover you can’t just say “implement XYZ” and see it working. You have to understand how things work, what is the best design, how to reach a certain level of performance. Then I compared the implementation, for correctness, to other systems, finding that other implementations sometimes contained more errors. I researched more, and found that the local inference world is full of subtle errors that accumulate and damage the model output, issues in the attention implementation causing performance slopes after the context is over a certain limit because indexed attention implementations are broken (do more work than they should, for instance), and so forth. This is the result of a domain that is very complicated to handle, fast changing, with models that are slightly different one from the other in the inference graph being released every day. It’s an unfair game for developers. Well: AI helps a lot with that. There are many domains where rigorous engineering (in the design side) and testing is *far* better than writing a GPU kernel by hand (or reading it). So are we sure most of that resistance it is not ideological? Matteo Collina yesterday asked me, in reply to my tweet: but didn’t you say that you check all the AI generated code for Redis? And this is a good question indeed. Yes, I do, but this is, at this point, something I *need* to do but that I believe to be mostly pointless, partially once GPT 5.5 was released, but now with Fable and GPT 5.6 Sol even more. Yes: I identify things that I don’t like how they are coded, but if I open other Redis files written by other Redis contributors there is *far worse*, and not since they are not good coders, but because it is a matter of taste. I write very clean code since I want it to be readable, so during the implementation of Redis Arrays I operated changes. I’m doing it again for the 50% memory saving optimization of Redis sorted sets, a PR that I’ll submit soon. But I do not feel this is useful anymore. Nobody should anymore look at this code, but only at the ideas the code contains. I continued to do it out of respect for users. Redis is at this point a commonly useful thing, and many programmers will open files and modify stuff by hand. But if I had my hands free, you know what I would do, instead? Use all the time that the review is taking me to do more QA, to think at the next optimization idea and apply it, and to use LLMs to write a DESIGN.md file where each data structure is described in human language, with the ideas it contains, the implementation tricks, the design. That, in the future, is going to be much more useful. Do you want to modify sorted sets? You open the file, read the design, then you own the ideas. You can open your agent and ask it what to do with the right mental model. This is a lot more useful than reviewing the code. Fable and GPT 5.6 reviews to the sorted sets memory saving are going to spot ways more errors and subtle race conditions that my review is going to uncover. Yet I’ll do it. But for the majority of software projects, all this does not make sense anymore. Focus on controlling the ideas, instead. Focus on quality, testing, and having an idea of the software you want to ship. The world changed and it is painful, but also full of opportunities to improve a software world that was already completely rotten. I have a doubt only regarding young programmers that don't have enough experience, and can't build a mental model. We don't know, yet, if they will require or not to understand very well how a given piece of code works, but I believe they should learn how to write programs. Yet, I'm not sure checking the LLM output is the right thing they should do. It may be a lot more useful if they learn some programming language and implement a small interpreter, a small database, an hash table and so forth. Reviewing some Javascript stuff of some web site for a customer? Hell, no, don't lose time with that shit. Comments

0 views
David Bushell 1 weeks ago

Astro is fine I guess

When I’m not fighting WordPress I deliver static HTML or the occasional JavaScript framework integration. For personal projects I have ‘fun’ with my own static site generator . This week was a side quest (soon to be main quest) to build my new company website. We’re talking proper business here so I can’t be messing about. I figured an off the shelf SSG would be most suitable. I asked the socials, “ 11ty or Astro ?” Both are popular but Astro had the edge. I gave Astro an early spin back in 2022 and found it slow . Maybe it’s good now? I ran with minimum release age to avoid immediately getting pwned . I selected Astro’s “Use minimal (empty) template” option and it generated both an and file — are you f — deep breaths, don’t fall for the rage bait. I code in a modern editor so I installed the recommended Astro extension. At first I struggled with Zed recognising HTML. I discovered a restart temporarily fixed the issue, but I guess I restarted one time too many because now the Astro LSP is completely broken. No modern comforts for me then. At least I can look at HTML without the red squigglies. I know what you’re going to say, “Dave bro, you’re inflicting this pain upon yourself! Just write HTML!” And I should. I just want native no-framework HTML includes , you know? Can you imagine the civilisation we’d live in if that could happen? I persevered and got my templates built with minimal fuss. I added a markdown collection and got the blog part blogging. It’s obvious that people use Astro to build real websites because all my “how do I” questions had an answer in the documentation. I’ve been forced to deploy way too many “React spaces” in my templates because Astro’s whitespace treatment is a mystery. I don’t need many components so I haven’t gone deep on Astro vs JSX . My site has zero JavaScript on the front-end. I plan to keep it that way. Edit: Christian Niklas on Mastodon shared a link to a recent Astro update where they added a option that defaults to no longer “following HTML rules.” Umm… okay. Set this to or if you’re building a website? I set it to . Minifying whitespace is over-optimisation. Astro has got the job done, despite the developer experience being broken out of the box. I dread to think what graveyard of dotfiles is installed if I choose a non-minimal start. I can easily de-Astro my templates should I need to. Right now Astro is solving the right problems and the issues are but a nuisance. Final conclusion: Astro is fine I guess. I’m not convinced Cloudflare’s acquisition is a good thing, considering their record for performative slop. I’ve lost my enthusiasm for DX and tooling to be honest. Even my own SSG experiments are collecting dust. I’d call the ecosystem a lost cause if I was being dramatic. I just try to avoid the worst of it and care about the end product: shipping a damn fine website! Which I can’t do because I’ve got more businessing to business before this particular site sets sail. Maybe in a few months? It’s looking awesome on though. Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds.

0 views
Ankur Sethi 1 weeks ago

Data locality (sometimes) beats algorithmic complexity

I've been ECS -curious ever since I learned about it in the Bevy game engine documentation . The ECS architecture predictably improves performance in languages that give you low-level control over memory (C, C++, Rust, Zig, and friends). But how does it fare when used in high-level, dynamic, garbage-collected languages such as JavaScript? This is the question Dan Murphy set out to answer in The Physics of Memory : Is it possible to use an ECS-style architecture in Javascript? And for applicable operations, does that actually do better than objects + V8’s garbage collection? To answer the question, Murphy built a 2D physics simulation of 15,000 balls bouncing around in a box using several different techniques. He found that a JavaScript implementation of the simulation that used ECS outperformed the usual "giant graph of objects" OOP implementation by 24x. He writes: It's also worth noting how the usual OOP implementation creates GC pressure: In OOP, entities are scattered across the heap. As they move and interact, the JavaScript engine’s garbage collector is constantly triggered, and the CPU frequently stalls waiting for pointer lookups. This causes sporadic frame drops (micro-stutter). Because ECS uses pre-allocated, flat TypedArrays, memory access is 100% predictable and GC overhead is zero, guaranteeing perfectly smooth frame delivery. My favorite thing about Murphy's post is that you can run all his benchmarks in your own browser. I love it when technical explanations or benchmarks are accompanied by embedded "apps" you can play around with. I'm surprised at how much data locality matters for performance. An algorithm with worse big-O complexity can outperform one with better complexity if it makes good use of the CPU's L1/L2 caches. Very cool. Cache Locality > Algorithmic Complexity : At 15,000 entities, pointer-chasing and unpredictable tree branching cannot compete with the contiguous L1/L2 cache locality of a flat 1D array sort—even though trees have a better theoretical Big-O complexity. You Don’t Need WASM for ECS Wins : Simply switching your JavaScript codebase to a flat Structure of Arrays (SoA) layout yields up to a  24x speedup  over OOP. WASM is the cherry on top (another 2.5x), not the entry ticket. Pragmatism Wins : While a hand-tuned SoA is the absolute fastest, using a production ECS library like   still gives you a massive  14x speedup  over OOP while providing a clean, scalable API. IMO, for 99% of applications using a library is the correct engineering choice.

0 views
Simon Willison 1 weeks ago

The new GPT-5.6 family: Luna, Terra, Sol

OpenAI's latest flagship model hit general availability this morning , and comes in three sizes: Luna, Terra, and Sol (from smallest to largest). The new models are priced per 1M input/output tokens as Luna $1/$6, Terra $2.50/$15, Sol $5/$30. For comparison, the Claude Opus series are $5/$25 and the Claude Fable 5 is $10/$50, but price-per-million tokens doesn't tell us much now that the number of reasoning tokens can differ so much between models for the same task. All three models have a February 16th 2026 knowledge cutoff, a million token context window, and 128,000 maximum output tokens. OpenAI's biggest benchmark claim concerns long-running agentic performance, with one benchmark showing all three models outperforming Claude Fable 5: We trained GPT-5.6 to get more useful work from every token. On Agents’ Last Exam , an evaluation of long-running professional workflows across 55 fields, GPT-5.6 Sol sets a new high of 53.6, eclipsing Claude Fable 5 (adaptive reasoning) by 13.1 points. Even at medium reasoning, it beats Fable 5 by 11.4 points at roughly one-quarter the estimated cost. That efficiency extends to smaller models, which are essential to making intelligence more abundant and affordable: GPT-5.6 Terra and GPT-5.6 Luna outperform Fable 5 at around one-sixteenth the cost. Amusingly, one self-reported benchmark that Fable 5 crushed the GPT-5.6 family on was SWE-Bench Pro, where Fable 5 got 80% compared to GPT-5.6 Sol getting 64.6%. This may help explain why OpenAI chose to publish this article yesterday specifically calling out SWE-Bench Pro for problems they found while auditing that benchmark: In light of these results, we estimate that ~30% of SWE-bench Pro tasks are broken, and advise that model developers carefully examine results I've had some early access to GPT-5.6 Sol - it's definitely very competent, though so far it hasn't struck me as better than Fable at the kind of complex coding tasks I've been using with Anthropic's model. As usual, the model guidance for using GPT-5.6 has the most interesting details. There are a bunch of new API features that I need to explore (and probably add support for in LLM ), including: Here's a full page with 18 different pelicans - for reasoning efforts none, low, medium, high, xhigh, and max across the three different models. It also lists their token and calculated costs - the least expensive was gpt-5.6-luna at effort none for 0.71 cents, the most expensive was gpt-5.6-sol at max reasoning level for 48.55 cents. In further pelican news, if you jump to 17:50 in their livestream from this morning you'll see OpenAI's own demo of 3D pelicans riding a tricycle, a bicycle, a pony, and another pelican! You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . Programmatic Tool Calling allows the models to "compose and run JavaScript that orchestrates tool calls" - which sounds to me like it could help bridge the gap between MCPs and full terminal sessions that can compose CLI utilities in useful ways. Also reminiscent of the dynamic filtering mechanism Anthropic added to their web search tool, which allows code execution against web results as part of a single model turn. Multi-agent lets the model "spin up subagents for parallel, focused work" - the sub-agent pattern now baked into the core API. Prompt cache breakpoints brings the Claude model of prompt caching to OpenAI, letting you be explicit about where the cache breakpoints are rather than relying on the API to detect them automatically. Personally I much prefer automatic detection (still supported by OpenAI), but presumably there are optimization cost savings to be had here if you put the work in. You can now set detail: original on image requests to avoid resizing the image at all before it is processed.

0 views
Farid Zakaria 1 weeks ago

Who does Anubis actually stop?

I have been working on a patch to the Linux kernel to support for the interpreter ( ) via bpf in [ thread ]. Of course I’m leveraging an LLM to help me do this! To pre-seed the context of the LLM, I asked it to read the https://lore.kernel.org/ thread. Uh oh. Looks like they have adopted Anubis , which is an HTTP proxy that requires proof-of-work before allowing access to the resource. Did this really do anything? Unfortunately, no. My AI diligently came up with anubis-fetch , which you can find at https://github.com/fzakaria/anubis-fetch . The tool tries to natively solve the proof of work or, as a last resort, will launch Chromium to visit the URL. This tool also impersonates a real Chrome TLS/JA3 fingerprint natively via req so it clears passive Cloudflare blocking too. ☝️ So who did we stop? The exact adversary Anubis targets defeats it trivially. The whole use of Anubis feels regressive and marginalizes those without access to “good” AI. For a scraper, solving the Anubis challenge is a one-time, amortized-to-zero cost since the cookie can be cached and reused. For a human, it’s seconds of spinner, battery drain on every fresh visit. They can’t amortize anything amongst each other. This “regressive tax” is paid even more so by those with weaker devices or who access the content on their phone. Clients that don’t leverage JavaScript (e.g., text browsers (w3m/lynx), screen readers, RSS readers) are completely left out. Did deploying Anubis stop any of the aforementioned bot-farms or are they mildly inconvenienced when they had to augment their bots to support a new proof of work solution briefly? The irony is that Anubis’s goal is to stop AI but it was incredibly easy for AI to circumvent it and yet the cost to humans and an open web remains. With the presumption Anubis is now a regressive tax, how much does it cost us? Every number here is a rough estimate. This is not a environmental argument at all since the bot-farmers and AI tools themselves are using many orders of magnitude more energy. Nevertheless, it’s interesting to see how much time is spent doing proof-of-work challenges that marginalize people. Difficulty is the number of leading zero hex characters the hash must have, so the expected work per solve is hashes. Difficulty 4 is the common default. Rates assumed: ~50 MH/s native (Go), ~0.5 MH/s in-browser JS; “felt” wall-clock includes page load, the worker, and the reload. Let be the number of Anubis challenge-solves per day, worldwide. Assume a felt time of and device energy per solve (screen + CPU). Collectively we are wasting an impressive amount of time waiting for access to websites; time we didn’t spend before the AI era. As a human, time is precious and finite to me, whereas to a robot it is not. Human-time / year = Energy / year (kWh) =

0 views
Takuya Matsuyama 2 weeks ago

Inkdrop Roadmap vol.6: Completed 🎉 — Now preparing for the official v6 release

Hi folks, it's Takuya here, the solo developer of Inkdrop . I'd like to report a status update on the Inkdrop project here. About a year and a half ago, I published the roadmap of Inkdrop vol.6 . And I'm happy to announce that every planned feature and improvement on that roadmap is now done! 🥳 They all shipped as part of the v6 canary series — 21 canary releases so far, built and tested together with the community. When I wrote the roadmap, I honestly wasn't sure how long it would take. I would have been surprised if the me of that time had seen this result. Thank you so much for all your feedback along the way — I couldn't have done it without you. Even beyond the roadmap, I've added so many new features and improvements. So, I'm confident you'll enjoy it if you're coming from v5. Let's dive into what I accomplished along the roadmap, what came out of it beyond the plan, and what's next. What made the development slow down was the huge technical debt, as I mentioned in the past post . Inkdrop was originally built on the Atom editor's framework, and when Atom was sunsetted in 2022, many of the modules it depended on were no longer maintained. I had to replace them one by one while keeping the app stable — the hardest and least visible part of this journey. With v6, that debt is finally paid off. Here's a quick before & after: None of these are shiny features on their own. But they're exactly what allowed me to ship everything you'll see below, and they make Inkdrop much faster to develop going forward. The codebase is now modern, healthy — and honestly, fun to work on again. I'm an indie developer, and Inkdrop is a one-person project — so manpower has always been the bottleneck. Paying off the tech debt was a particularly big headache: some of the inherited modules were so large that it originally took the whole Atom team to maintain them. But thanks to the recent advancements in coding agents, that burden finally feels manageable — and even enjoyable to tackle. AI didn't just speed up the coding; it changed how I work: These new workflows have opened up possibilities that simply didn't exist for solo developers before. A refactoring of this scale used to be unthinkable for one person — now I can maintain a codebase that once took a team, and spend the saved energy on what matters most: the product itself and my users. Here's the roadmap vol.6, item by item, with what actually shipped: The roadmap was only half the story. While working through it, I ended up rebuilding a huge part of the app and shipping a lot of features that weren't planned. Here are the highlights, grouped by area: And on top of all that, hundreds of bug fixes reported by canary testers. The community has also been building amazing plugins on the new APIs — note-tabs (browser-like note tabs), code-runner (run JS/Python code blocks in notes), constellation (an interactive note graph), copy-as-jira , kanso-ink (theme), and more. Existing plugins are getting v6 support too, like hitahint , link-compact , thumbnail-list , and editor-utils . My goal remains the same as I wrote in the roadmap: keep improving the core user experience without bloating the app, so you can stay focused on taking notes. I believe v6 embodies exactly that. You can download the binary here: Please create a topic on the “ Issues > Canary ” category. This is the most preferred way for me because I can manage which issue has been resolved or not. We have our Discord server , where you can casually discuss and talk with other users. With the roadmap completed, I've shifted gears to preparing for the official release of v6 . That means polishing the details, stabilizing the canary builds, updating the documentation and the website, and helping plugin and theme authors migrate. Especially, building a new landing page is gonna be fun! I'm also going to work on the mobile app as well. The official v6 release is getting close. Stay tuned! 💪 I manage implementation plans as Inkdrop notes and let the agents work through them. Watch: Note-driven agentic coding workflow using Claude Code and Inkdrop I built and published a tool to manage multiple Claude Code sessions on tmux . While building the AI features, I had an agent explore Zed's source code and save the report to Inkdrop , to learn how it implements similar functionality. ✅ Share target & share extension — You can quickly stock web pages into Inkdrop from other apps on mobile. ( v5.5.0 ) ✅ Command palette — It became Telescope , a versatile Spotlight-like search bar (the name is borrowed from telescope.nvim, haha). It fuzzy-searches commands, notebooks, tags, and the table of contents of the current note, with scope prefixes like for commands and for notebooks. It's extensible, so plugins can add custom sources. ( canary.1 ) ✅ Migrate to CodeMirror 6 — The biggest one. The whole editor was rebuilt on CodeMirror 6, and it enabled a bunch of new editing features: a floating toolbar, slash commands, GitHub Alerts syntax support, emoji autocompletion, autocompletion inside code blocks, and quick note-link insertion with . ( canary.1 ) ✅ Outline view — Powered by Telescope. Click the button in the editor header (or run ) to jump between sections. It highlights the current section based on your cursor or scroll position, and even lists task items. It's provided as a plugin ( telescope-toc ), which doubles as a reference implementation for custom Telescope sources. (Thanks Basyura-san for the original sidetoc plugin!) ( canary.6 ) ✅ Preview pane improvements — Copy buttons for code blocks landed in both the preview and the editor, and double-clicking an image opens it in an image viewer. As a bonus, find-in-preview finally works — it highlights matches even across DOM elements, which is essential for finding text in code blocks. (Thanks q1701 and Basyura for the original plugins!) ( canary.2 , canary.4 ) ✅ Two-factor authentication — OTP-based 2FA is available for your account. ( v5.11.0 ) ✅ Prepare for ARM64 & other platforms — This required repaying a lot of technical debt. I replaced the deprecated LevelDB backing store with SQLite , stopped bundling (which used to bundle all of Node.js and npm!), and rebuilt it as a lightweight standalone CLI ( @inkdropapp/ipm-cli ). As a result, Inkdrop now supports ARM64 on Windows and Linux , plus Flatpak and AppImage packages for modern Linux distros. ( canary.1 , canary.4 , canary.5 ) ✅ Improve image upload speed — Attachments are now uploaded in parallel via signed URLs, so syncing image-heavy notes is significantly faster. ( canary.12 ) ✅ Diff view for revision history on desktop — The diff view I loved on mobile is now on desktop, too. ✅ Notebook icons — You can assign custom icons to notebooks from a picker with 1,500+ icons from the Lucide icon set, with category tabs and search. Icons show up everywhere — the sidebar, Telescope, and notebook selectors. ( canary.9 ) ✅ Visualize your progress and achievements — The activity stats view shows how many notes you created and tasks you worked on over the past 52 weeks, along with your current and longest streaks. Note-taking is a contribution to your work, after all! ( canary.14 ) ✅ AI integrations — Shipped as an opt-in, bring-your-own-API-key design, so you stay in control of your data. The inline AI assistant transforms selected text in place with built-in prompt presets (proofread, summarize, Mermaid diagrams, Markdown tables, and your own custom prompts). Next Edit Suggestions predicts your next edit like GitHub Copilot — set to manual trigger by default so it doesn't distract you — and it can even draw context from your linked notes and backlinks. ( canary.16 , canary.18 , canary.20 ) Reading highlights — Select text and hit the highlight button to wrap it in a tag, rendered beautifully in the preview. Perfect for emphasizing what resonates in your reading notes. ( canary.3 ) Native spellcheck support — The editor now uses the OS-native spellchecker. ( canary.10 ) Smarter link pasting — Pasting a URL now suggests link formats inline through the autocompletion menu instead of a dialog, and the page title is fetched in the background so nothing interrupts your flow. ( canary.15 ) Create a note from autocomplete — Start typing a title after , choose "Create new note," and it's created, linked, and opened in one step. ( canary.16 ) Little things that add up — ToDo item strikethrough, link-open tooltips, commands (Thanks Lukas and TheRabidOstrich !), View menu toggles for line numbers / line wrapping / readable line length, and a refurbished editor header with navigation back/forward, view mode buttons, and a native action menu (Cmd/Ctrl+J). ( canary.2 , canary.3 , canary.12 , canary.18 ) Embed GitHub code snippets by pasting a link — Paste a GitHub source URL and the code is fetched and inserted as a syntax-highlighted snippet with line numbers and a link back to the source. Connect your GitHub account via OAuth and it works with private repos too, including rich link titles for repos, issues, and PRs. ( canary.6 , canary.11 ) Advanced code blocks — Language icons, line numbers, and meta info rendering, plus GFM highlighting inside fenced code blocks — nested code blocks and YAML frontmatter included. ( canary.6 , canary.9 , canary.20 ) Mermaid got a serious upgrade — A pan & zoom toolbar with a full-screen viewer, and diagrams are now themed entirely through CSS variables, so they automatically match your theme in light and dark mode. (Thanks @inkwadra for the original pan/zoom PR!) ( canary.21 ) Manual notebook ordering — Drag and drop notebooks in the sidebar into your preferred order; it syncs across devices. ( canary.9 ) Fuzzy matching everywhere — Telescope, the notebook and tag list menus, and the tag input all use the same fuzzy-matching algorithm, so you find things fast without spelling them right. ( canary.15 ) Quicker navigation — Filter buttons for notebooks and tags in the sidebar, a search bar in the notebook picker, context menus on the workspace and note-list headers, and a sort-order button that shows the current order as a label. ( canary.6 , canary.15 , canary.16 ) Keep running in the system tray (Windows & Linux) — Handy if you use the local HTTP API, and it makes reopening the app instant. (Thanks Kyoichiro-san and Micha for the request!) ( canary.21 ) Plus a custom-built tooltip UI, a macOS "Look Up Selection" context menu, and an account usage stats tab. ( canary.14 , canary.16 ) A new CSS-variable-based theming system — Themes are now a thin layer of variables over the base styles instead of a full Semantic UI stylesheet, which makes them far easier to build and maintain. ( canary.18 ) One theme package instead of three — The UI / syntax / preview theme types inherited from Atom have been merged into a single unified package that styles the whole app. ( canary.21 ) Live theme previews — The Themes preferences show preview cards rendered live from each theme's color palette, and is uploaded to the plugin registry to power previews before you install. ( canary.20 , canary.21 ) New official themes — Kanagawa ( Wave / Dragon / Lotus ), Solarized ( Light / Dark ), and Nord ( Dark / Light ), plus a default syntax theme overhaul built on modern CSS like . ( canary.18 , canary.20 , canary.21 ) Dropped Electron's module — I replaced it with type-safe IPC bridges in a massive architectural overhaul. Database access from plugins became roughly 13x faster , and the app is more secure because only intended methods are exposed. ( canary.11 ) SQLite as the backing store — Replacing the long-deprecated LevelDB unblocked ARM64 support and repaid one of the oldest debts from the Atom era. ( canary.4 ) Modern build pipeline — Migrated from Webpack + Grunt to electron-vite (Vite + Rolldown), which made production builds 10x faster and the dev build launch almost instant. I also converted all Less stylesheets to plain CSS, moved drag & drop from the unmaintained to , and kept Electron riding the latest releases throughout the canary series. ( canary.14 , canary.18 ) Security hardening — Access keys moved to the system keyring, and the login flow is protected with Cloudflare Turnstile against credential-stuffing bots. ( canary.16 , Security Update ) A brand-new CLI — No more bundled Node.js and npm. It publishes tarballs directly like npm (no more committing compiled files to GitHub), and scaffolds a new plugin or theme in seconds with TypeScript all wired up. ( canary.5 , canary.18 ) Official TypeScript definitions — @inkdropapp/types gives plugin authors full type safety without exposing the app's internals. ( canary.14 ) Auto-installed essential plugins — mermaid, math, and markdown-emoji are installed and kept up to date automatically, and you can disable them anytime. ( canary.14 ) Vim plugin improvements — Relative line numbers (Thanks @p1n9_d3v !) and an option to keep Vim registers separate from the system clipboard (Thanks @birtles !). ( canary.11 ) Updated docs — The plugin migration guide and theme development guide are refreshed for v6, along with new component and module references. https://my.inkdrop.app/download/canary Inkdrop Website: https://www.inkdrop.app/ Send feedback: https://forum.inkdrop.app/ Join the Discord server: https://docs.inkdrop.app/start-guide/join-discord-server 𝕏: https://x.com/inkdrop_app 🦋: https://bsky.app/profile/devaslife.bsky.social

0 views
Jim Nielsen 2 weeks ago

Making a Shuffle Button

I made some updates to my notes blog , including a change to how my “Shuffle” feature worked. Figured I’d blog about it. At the time of this writing, I have 974 “notes” that I’ve published. For fun, I have a “shuffle” button that digs up a random note from the past. I like to press it from time to time and re-encounter some insight from the past. It’s like going through an old album, pulling out a random photo, and thinking, “Oh yeah, I remember this! Good times.” Like old photos, there’s also the occasional “that didn’t age so well”. But I find it fun to randomly dig up old insights from others and continue to be inspired. Since my site is built and hosted as static files without a runtime server, this feature required JavaScript to work. Every page had a snippet like this: Essentially: inject every note ID into every HTML page and, when the shuffle button is clicked, randomly grab one and navigate the user to it. Not the most elegant thing, but it worked. The problem was that every time I published a new post, every single page had to be re-uploaded to Netlify because every file’s hash would change and its etag/cache was invalidated. This made my builds slow. It also made it difficult, from a development perspective, to ensure refactors didn’t result in unexpected changes to output (using from my SSG web origami ). So I decided to make a change. Because I love to see if I can make things work without JavaScript, I had the thought to randomly write the at build time using my SSG, which would result in output like this: And every time I re-build my site, just have this logic run on the static site generator so that it’s different for every page, every time. I decided I didn’t want to do this, so on to JavaScript! My first thought was to create a single JSON file that contained all my note IDs. Then when the “Shuffle” button gets clicked, I fetch that, grab a random ID, and navigate the user, e.g. This would work. It localizes the caching issue to a single file, so only one file has to be invalidated/re-uploaded across builds. But in playing with it a little more, I decided to try something a little more...unconventional. I’ve written before about having lots of little HTML pages and I thought, “Can I put this functionality in a single HTML page rather than a JSON file?” And what I ended up with was a link, e.g. That when clicked navigates the user to a new page. That page has all the JS logic embedded in it, e.g. There are a few things I like about the experience this implementation provides. First: shuffle is a route , so I can navigate to it directly without using the GUI, e.g. notes.jim-nielsen.com/shuffle Second: I handle the UI/X with a slight delay to make it appear like something is happening when you click the button. If you click the button and it immediately jumps to the next, randomized page, it almost seems to happen too fast. Like you’re left with this feeling of “What just happened?” But in this scenario, it navigates you to the “Shuffle” page, the button you just clicked turns into a spinner + text indicating something is happening, and there’s a slight (intentional) delay before the JS executes and sends you to a randomized note. I know it’s a bit weird. “Introduce artificial slowness? Are you crazy?” But I like it. It feels like the shuffle feature on an old music player. I remember one of my CD players had a “Shuffle” feature. When I’d click the button, it would display “Shuffling…” on the little black and white screen and you’d encounter this brief state where (I presume) the lens inside the hardware would move along the physical track to the spot where it would start reading a new, random song from the CD. The hardware constraints necessitated this kind of an experience, but I always liked it because it felt like the CD player was “thinking” about what track to pick next. This state clearly conveyed to me that my intent to shuffle was received and being followed. I liked that feedback, and it’s exactly what I wanted to do on my notes site (even though it was completely unnecessary). I like having that brief moment of feedback where it’s very clear that your intention was received and being followed, vs. having it happen so fast you can’t even perceive precisely what happened. Here’s a video to show it in action: I know that’s a lot of information for something so small — and, arguably, unnecessary. But I still enjoy writing about how I make decisions when I build things for myself. Hence this post. Reply via: Email · Mastodon · Bluesky Doesn’t require JavaScript Doesn’t require a server (request-time logic) File hashes change across builds (even if there’s no new content or template changes, every HTML page now has a different for the shuffle link for every build ). This makes deployments way slower because Netlify has to redeploy every file on every build. Plus Etags change so caching is basically ineffectual.

0 views
Max Bernstein 2 weeks ago

Travel notes: PLDI Boulder

I had another excellent PLDI this past June. It was my fourth 1 . I continued to meet new people and learn new things! Overall: I got to meet a lot of new people, which was exciting. I had some good chats about research. I asked a question at a talk! I got to show Aaron and Jacob PLDI and see them enjoy it. I missed hanging out with CF Bolz-Tereick and Chris Fallin, the usual suspects at conferences I attend. I’m looking forward to next year. This post is more about the conference than the town of Boulder (unlike the last PLDI post about Seoul) because I didn’t do much Boulder exploring. I got in late on Sunday. Then I had to take a long car ride from Denver airport to Boulder. I don’t think I had ever flown into Denver with intent to go to Boulder before so it was a bit of a surprise. Jacob offered to have a late dinner with me so we had a tasty meal at Gaia Masala and Burger. Shout out to Harry, our server. Monday was a workshop day. I signed up for EGRAPHS and mostly stayed in that workshop. People kept throwing around the term “Knuth-Bendix”, as they have been for several years, and only in one of these workshop talks did someone explain it in a way that made any sense at all. It seems kind of like equality saturation but for the rewrite rules themselves—no actual expression graphs involved. I DMed Phil this sketchy explanation during a talk to get his response and I got to watch him cock his head and think about it in real time. At lunch I met Qiantan Hong and we got to talking about Common Lisp and its object system, CLOS. Seems like a combination of ahead-of-time compilation and multiple dispatch is really tricky. I had dinner with Aaron at Postino and then wandered into a bunch of people staying at the conference hotel chatting in the lobby. Ben Titzer said “fix my subtyping bug”, which I interpreted as him saying hi. I ended up just planting myself at the table as a bunch of interesting people cycled through: Jared Roesch, Mae Milano, Hila Peleg, Russel Arbore. It was a late evening. Back to the workshops! But late because of aforementioned late evening. I saw Vadym’s talk about Remora. I only understood about 40% of it but it was good to catch up. I hadn’t seen him since leaving Northeastern. Around a break time I joined a little cluster of people talking about e-graphs and I guessed asked enough basic questions that Pavel convinced Max Willsey to run a “BYOEG” (build your own e-graph) tutorial. The structure was as follows: Max would instruct Jacob as to what kind of thing to build next but not be prescriptive about exactly how to build it and not look at Jacob’s screen. The rest of us would sit around a table and try to follow along as best we could. I hear Pavel has a blog post about this experience coming soon… I saw Slava Pestov walking around and introduced myself because we keep liking one others’ bad jokes on Mastodon. We ended up getting dinner with Aaron and Jacob that night at Leaf. We learned a lot about monoids, Knuth-Bendix (!), Factor, and Swift. Slava volunteered to do a similar follow-along “BYOKBC” (build your own Knuth-Bendix completion) tutorial the next day. First day of the conference! I was walking into the hotel in the morning and I had made it about three feet onto the property when Alexa VanHattum, who was going the opposite direction, convinced me to instead get coffee elsewhere. We had a nice catch up and I got to hear about what teaching is like these days. Lunch was fun. I got to do another round of “ambush person whose research I admire” and plopped down with Ben and Christian Wimmer. I’d spent a lot of time struggling with Christian’s papers on linear scan, then convinced him to chat about register allocation with us on a video call a couple of months ago. We continued some of that at lunch but then I (kind of accidentally kind of on purpose) got Ben started talking about Sea of Nodes and how it is and is not different between Java and (for example stand-in for dynamic languages) JavaScript. Apparently he is thinking about a similar thing that he is calling Sea of Variables. We talked about inlining challenges and how to infuse profiles with call context, which can be a challenge. I feel more inspired to get type-based alias analysis working in ZJIT. I tracked down Christian later in the courtyard and got to hear about what he’s working on these days. I know very little about ML compilers and ML hardware and things like that so hearing about the challenges was neat. Yannis Smaragdakis joined our little standing table chat and we got to learn about Datalog. Because I had previously written about linear scan register allocation and about liveness analysis with Datalog, I goaded him into pairing with me on writing a full linear scan implementation in Datalog. This ended up taking the rest of the evening and several beers and then a lot of the next day! And after the first bit of code I did not manage to contribute very much at all. I met Hannah Gommerstadt and we got to chatting about bikes and formal methods (separately). Slava walked by and I got to introduce them. Then Jacob too. I continued pairing with Yannis but remained really lost. The only thing I think I contributed was some familiarity with the core algorithm, which he had only really seen in passing before. Eventually he got it fully working, but it needed some deep trickery. More on this soon in its own post. I saw a talk about versioned e-graphs and that got me wondering if their implementation can be used as a persistent e-graph or even just persistent union-find. Sometimes you want to do backtracking, or have undo-redo in your compiler. Then I went to a talk about streaming byte-pair encoding (BPE). BPE is hard to do streaming because it definitionally requires looking over the whole input string. They did some neat trickery to find boundaries in the string that demarcate regions that don’t interfere with one another and thereby tokenize on-the-fly. I didn’t understand it fully but I asked my only question of the conference, which was if this could also be used to implement BPE in parallel. Seems the answer is “maybe” so I should probably reach out and ask further. Slava started showing me and Jacob and Aaron how to implement Knuth-Bendix completion for strings. I had a lot of tiny little bugs which slowed progress. Such is life. The banquet and awards ceremony started so we called it a night and went off to eat dinner. They had good lentils. I ran into Thalia Archibald and John Regehr and we talked about (really, they talked about and I tried to learn something) what it might mean to either port Alive2 to another compiler than LLVM, or build “Alive3”, or “Mini-Alive” for some other IR. John suggested fuzzing the hell out of the thing first, then doing something more formal later… especially if it’s a dynamic language IR where a lot of the opcodes end up being “function call that can do anything”. I had a nice chat with Steven Holtzen and Zach Tatlock about research and grad student life. I got some good advice. I meant to talk to Zach about this thing we keep occasionally chatting about that I call “the big e-graph in the sky”. I talked a bit about it to Max Willsey and he had some good probing questions about what would be slow, challenging, or somehow undefined given my problem statement. I continued struggling to implement Knuth-Bendix with significant assistance from Slava and I think eventually got something working. I had a really nasty bug due to string slicing semantics 2 in Ruby. Aaron went off to learn about deep immutability in Python and then got to chatting with the authors of the paper. We got to compare notes about language and language implementation challenges. It’s been a long time since I was in Python-land. Aaron and Slava and I got enchiladas for dinner. I had no reading material for the flight so I went downtown, intending to buy one book, but got too many books. They barely fit into my bag for the flight home. I started reading Anathem for the second time. It holds up. It’s a damn good book. Actually, it might be my fifth. I just remembered that I attended PLMW in 2020 and also watched a few online talks at wild hours from my living room.  ↩ The semantics in Ruby are probably globally reasonable but did not fit the thing I was trying to do: if we have two strings and , we want to find the index at which they start to overlap, . Then we want to grab the bit of that is to the left of . I had initially written that as . However, if and overlap at the start of , is 0. This generates the range , which means we’ll slice until the end of . Not what we want. Instead, the fix in the commit shows how I had to add a special slice function called that handles the 0 case.  ↩ Actually, it might be my fifth. I just remembered that I attended PLMW in 2020 and also watched a few online talks at wild hours from my living room.  ↩ The semantics in Ruby are probably globally reasonable but did not fit the thing I was trying to do: if we have two strings and , we want to find the index at which they start to overlap, . Then we want to grab the bit of that is to the left of . I had initially written that as . However, if and overlap at the start of , is 0. This generates the range , which means we’ll slice until the end of . Not what we want. Instead, the fix in the commit shows how I had to add a special slice function called that handles the 0 case.  ↩

0 views
マリウス 2 weeks ago

Making My Content More Easily Digestible

Over the past few months a recurring theme has emerged in my inbox, as well as within the community channel , and it is one that I have been chewing on for a while now. Several of you have, kindly and very politely, told me more or less the same thing, which is that even though the topics I write about are interesting enough, the posts themselves have grown so long and so dense that actually getting through one of them has turned into something of a commitment rather than the casual read it probably ought to be. I cannot really argue with that, because it is true. Whenever I sit down to write about something like Bureaucracy is Eating the World , or A Word on Omarchy , or Doubting Your Favorite Web Search Engine , I find myself pulled in two directions at once. On the one hand I want to be accurate and diligent, which in practice means citing my sources, anticipating the counter-arguments, and walking through the reasoning (and in many cases evidence) step by step instead of asking the reader to simply take my word for it. On the other hand I am painfully aware that the result of all that diligence is, more often than not, a wall of text that runs to several thousand words. In those posts in particular I clearly landed on the wrong side of that trade-off, and the feedback was entirely fair. The information is, I think, worth having, but the packaging asks a lot of the reader, and not everyone who might benefit from the content has the hour or so of uninterrupted attention that getting through it properly demands. Note: Yes, I am perfectly aware of the irony of writing a not-exactly-short post about how my posts have become too long, but bear with me here for a moment. What the feedback really did, though, was hand me an idea. Rather than butchering the original write-ups down to a length at which they would lose the very nuance that justified writing them in the first place, I figured I could instead try to produce a second, more compact version of my most detailed pieces. One that lives alongside the original rather than replacing it. I decided to start with Bureaucracy is Eating the World simply because it is one of the longest, and densest, and newest write-ups. And when you ask yourself what tends to be more digestible than a multi-thousand-word essay, the answer that most people arrive at almost immediately is video and audio , both of which you can consume while doing the dishes, commuting, or otherwise not staring at a screen full of paragraphs. So I started fiddling around with a whole handful of different programs and apps, trying to work out a reasonable pipeline for turning the written text into something more compact and considerably easier to consume, and it turned into a much deeper rabbit hole than I had naively assumed it would be when I started. The first piece of the puzzle was the narration, and here I worked my way through a zoo of “AI” text-to-speech services before ultimately settling on a service called ElevenLabs to generate the spoken version of the existing post, mostly because the quality of the output was, to my ears at least, the least robotic and the easiest to listen to for any extended stretch of time. Now, the obvious question, is why I would hand my own words over to a machine to read out loud rather than simply recording myself, which would arguably be more authentic and would certainly have involved less fiddling. The answer, predictably for anyone who has read more than a post or two on here, is privacy . Your voice is not merely a sound, it is a biometric identifier, just as much as your fingerprint or the geometry of your face, and the moment you put a sufficiently long, clean recording of it onto the public internet you have effectively handed anyone who cares to grab it the raw material they need to clone it. Voice cloning has, over the past couple of years, gone from an expensive novelty to something that runs on consumer hardware off a few seconds of reference audio, and it is already being used in the wild to defraud people, whether that takes the shape of the classic “grandchild in trouble, please wire money” phone call, or the more targeted corporate variety in which an employee approves a transfer because the “CEO” apparently rang and asked them to. On top of the outright fraud there is the machinery of surveillance capitalism, which will happily fold a voiceprint into the (shadow-)profile it is already busy assembling on every single one of us, cross-reference it against the recordings collected by smart speakers, call centres, telecommunication companies, and who knows what else, and then use it as yet another durable identifier that follows you around regardless of which account you happen to be logged into or not. I am simply not willing to surrender my right to my own voice, along with a measurable chunk of my privacy, in exchange for the modest convenience of having a blog post read aloud, especially not when a machine can today do that very job equally well and at a quality that is, for this particular purpose, entirely sufficient. With the audio sorted, I needed something for the viewer to actually look at, and this is where the project spiralled into something far more involved than I had anticipated. My initial plan was to do everything in Blender , which is the obvious, powerful, free and open-source choice, but the learning curve on Blender is famously steep, and after a few evenings of mostly fumbling around I had to be honest with myself about the fact that I was spending far more time fighting the software than producing anything watchable. I therefore ended up reaching instead for Source Filmmaker , or SFM , the slightly ancient animation tool that Valve built on top of the Source engine, purely because its learning curve is so much gentler than Blender ’s and because it let me get the job done without first having to become a 3D animation expert. Where things became tedious, however, was the animation itself. My first instinct was to take the lazy route and let motion capture ( “mocap” ) do the heavy lifting, so I gave Rokoko ’s video-to-mocap tool a try, hoping that I could simply feed it some footage and get usable animation data back out, but it failed pretty miserably, probably because I didn’t have the space nor the equipment (multiple cameras) to set it up properly. I then went looking for alternatives, and discovered that you can, for instance, pair an old Xbox Kinect with various bits of software (the likes of Brekel ) that are able to spit out FBX files, which in turn can be used to drive the characters. The catch is that the pipeline of exporting the SFM animation, importing it into Blender , and then using Rokoko ’s retargeting plugin to map the captured motion onto the SFM model is a fiddly, multi-step affair, and the end result, no matter how patiently you tweak it, will never come close to what you would get out of Rokoko ’s actual motion-capture suit and gloves, which I do not own and was not about to buy for a single experimental video. So I abandoned the shortcuts altogether and animated every sequence by hand instead, and even though the individual sequences are fairly simple and relatively short, doing it this way still took a considerable amount of time and not a small amount of patience. SFM is, after all, a fairly old piece of software that carries a noticeable amount of quirks, and the StarBook that I happened to be running this entire experiment on was, to put it generously, never the right tool for 3D animation work in the first place. To make matters slightly worse, I was unable to coax SFM into exporting anything above 720p, no matter how I adjusted its startup parameters, because anything beyond that resolution would come out glitchy and unusable, so 720p is, for this first attempt at least, simply what we are working with. All of this rather long-winded preamble is simply to say that what follows below is a first experimental attempt at presenting one of my denser posts in a format that some might find easier to digest than the original wall of text, in the hope that it piques the interest to dive deeper into the topic. The whole point of this is to find out whether the slice of my readership that feels buried under several thousand words actually prefers something like this, or whether the effort is better spent elsewhere. Keep in mind that the video is nevertheless a compressed version of the original post, that does not include every little detail, as it would have otherwise, too, grow out of proportion. You can find the result here , or, if you happen to have JavaScript enabled despite my warnings , below: If the response is positive, then I might well turn these into a more regular thing. If it is not, then at the very least I will have learned a fair bit about text-to-speech, Source Filmmaker , and the dark art of motion capture along the way, which is hardly the worst outcome. Either way, I would very much appreciate your honest feedback on this, so please do let me know what you think, whether this format could work, whether the pacing and the visuals help or hinder, and whether this is something you would like to see more of going forward. As always, you know where to find me .

0 views
David Dodda 2 weeks ago

Why Don’t Websites Put All Their Images Into One Giant JPEG? (Nerd-Sniped by My Brain)

I had a simple question: Why do websites load lots of individual images instead of stitching them into one giant image and cropping out the pieces they need? At first glance, an image atlas sounds great. Instead of this: You create this: Then each UI tile crops a specific region from the atlas. That would mean: fewer network requests images arrive together no staggered popping maybe better perceived loading maybe less request overhead Not a new idea by any means. Games and UI libraries have used sprite sheets and texture atlases forever. The question is: why isn’t this the default for websites? I compared three approaches: Individual optimized images 14 separate optimized JPG files rendered as normal elements Canvas atlas one stitched atlas JPG each tile rendered by cropping from the atlas into CSS background atlas one stitched atlas JPG each tile rendered with , , and The atlas was regenerated from the same optimized images, so the comparison was more fair. NOTE: I ran the experiment by hosting it locally. so all the number you see are when you have the application served using a python server running locally. If you want to poke at it yourself, the experiment is live here: https://daviddodda.com/experiments/img-atlas/ note: make sure you disable cache. try each version a couple of times. I focused on three headline metrics. How many bytes were downloaded? When did the last required image resource finish downloading? When was the image grid actually ready to see? This last one matters because network completion is not the full story. The browser still has to decode images, rasterize, paint, composite, and show pixels. On a remote machine running Chromium, all files hosted locally, 10 runs each: The surprising result: The CSS background atlas was the fastest to visible. The atlas had a clear network advantage: Well, one larger request has less overhead than many smaller requests. This effect is especially visible when the server/browser are using less optimal connection behavior. In my test, Chromium reported for the local server, so request overhead was more obvious than it would be under HTTP/2 or HTTP/3. With modern HTTP/2 and HTTP/3, many individual image requests are less painful because requests can be multiplexed over one connection. But request overhead still exists. The individual images transferred: The regenerated atlas transferred: Because an atlas is a rectangle. Real images have different aspect ratios. When you pack them into one big rectangular sheet, you often create empty space. In my case: That is about 31% extra pixel area. So even though the atlas used one request, it transferred more data and required the browser to decode a bigger image surface. The canvas atlas looked like it should be fast (thought modern hardware was fast enough). It loaded one atlas image, then cropped each tile into a canvas. But the results were poor: The breakdown showed: The actual JavaScript canvas drawing was not expensive. The expensive part was making all those canvas results visible. That means the bottleneck was not: It was the browser’s later paint/composite work. The CSS background atlas used normal DOM elements: This was much faster: The breakdown: The decode cost was still there. But paint/composite was dramatically better than the canvas version. So if you are going to do image atlasing in normal web UI, CSS backgrounds may be much better than drawing many cropped canvases. They are great for: emoji sheets game textures small repeated UI assets known fixed-size tile sets maps or tile-like interfaces cases where all assets are needed immediately They are less great for: photo galleries blog images user-generated content responsive images content-heavy websites long scrolling pages frequently changing assets now, don't go getting any ideas about rewriting your website's image pipeline to use image atlas. here are some reason why it's a really bad idea. With individual images, the browser can load only what is needed: With a giant atlas, loading one image means loading everything in that atlas. That is great if you need everything immediately. It is terrible if the user only sees 5% of the images. The web has powerful responsive image tools: The browser can choose the right image for the device, viewport, DPR, and network. With a giant atlas, this becomes much harder. You may need multiple atlases: The combinatorial complexity gets ugly quickly. Atlases require packing. Packing creates waste. If the images have different shapes, the atlas may contain a lot of empty or unused area. Even a good packing algorithm cannot always avoid this. In my test, the atlas had about 31% more pixel area than the individual images. With individual images: Only that image needs a new URL/cache entry. With an atlas: The whole atlas cache is invalidated. That is bad for websites where content changes often. Browsers are good at prioritizing resources. The hero image can be high priority. Below-the-fold images can be lazy. Tiny thumbnails can wait. With a giant atlas, everything has one priority. You cannot easily say: The atlas is all-or-nothing. A compressed JPG might be 2 MB on the network, but decoded pixels are much larger. Decoded RGBA memory is roughly: A large atlas can become a huge decoded surface. In my first broken atlas attempt, the atlas was: That is around: Even if the file downloads quickly, that is a lot for the browser to decode, rasterize, and paint. An has natural semantics: A CSS background image is decorative by default. If the image is meaningful content, you need to rebuild semantics with ARIA or hidden text. That is doable, but it is extra work and easier to get wrong. Browsers have spent decades optimizing: If you use an atlas, you bypass some of that machinery and take on more responsibility yourself. Sometimes that is worth it. Often it is not. Every approach has its niche use case (shocker). My brain nerd-sniped me into exploring and writing about this. It was fun seeing the cute animals load in though. fewer network requests images arrive together no staggered popping maybe better perceived loading maybe less request overhead Individual optimized images 14 separate optimized JPG files rendered as normal elements Canvas atlas one stitched atlas JPG each tile rendered by cropping from the atlas into CSS background atlas one stitched atlas JPG each tile rendered with , , and emoji sheets game textures small repeated UI assets known fixed-size tile sets maps or tile-like interfaces cases where all assets are needed immediately photo galleries blog images user-generated content responsive images content-heavy websites long scrolling pages frequently changing assets

0 views
David Bushell 2 weeks ago

The modern app

Today I’m introducing the next generation of code editor. A modern app to satiate the needs of the discerning coder. We’re talkin’ blazing fast collaboration between man and machine . Try out the demo below (for best experience: desktop Chrome, obvs). If you’re reading this in RSS I have no clue what you’re about to see… maybe visit the demo it’s a fun one! Update ready (restart required) A modern app requires JavaScript, bro. Error loading documentation. Please disable your adblocker and try again. We and our 9172 partners value your personal data. You must accept the terms and conditions. Application error: a client-side exception has occurred (see the browser console for more information). Last edit: DELETED USER – 1 January 1970 – is this working? abandonment abbreviation aerodynamically antidisestablishmentarianism [Advertisement: 1% off subscription] an icon bar full of indecipherable icons with no label > Activate Windows Go to Settings to activate Windows. Fix the bug and make no mistake The user has asked me to fix his shitty code and to “make no mistake”… is he stupid? Thinking harder… On second review his code is garbage slop, should I search the internet and plagiarise ZA̡͊͠͝LGΌ ISͮ̂ TO͇̹̺ͅƝ̴ȳ̳ TH̘Ë͖́̉ ͠P̯͍̭O̚​N̐Y̡ H̸̡̪̯ͨ͊̽̅̾̎Ȩ̬̩̾͛ͪ̈́̀́͘ ̶̧̨̱̹̭̯ͧ̾ͬC̷̙̲̝͖ͭ̏ͥͮ͟Oͮ͏̮̪̝͍M̲̖͊̒ͪͩͬ̚̚͜Ȇ̴̟̟͙̞ͩ͌͝S̨̥̫͎̭ͯ̿̔̀ͅ Would you like to play a game? Thinking… Family photos were deleted to resolve low disk space error. iOS 26.6.9 is available, update now? Amazon driver is lost in your neighbourhood. Alice’s personal access token expired, switching to Bob’s. Tailwind language server crashed. SAMSUNG SMART REFRIDGERATOR ® has detected low milk levels: five gallons ordered. 418 I’m a teapot. close AI, AI, AI! We’ve heard you. There are now 26 new sparkle buttons! Can you find them all? Release notes dialog now has an embedded WSL 1.0 terminal emulator. It’s broken (issue: #25293). Reduced RAM usage when typing on the home row. Keystrokes are now logged in the correct Slack channel (fixes #7 and #933 through #980). root@localhost system32 C:\ $ _ Yeah so um… have you noticed that all modern software is teetering on the enshitty cliff? Everything in my dock is an Electron-ified enshittybomb one update from disaster. There used to be alternatives. Now those suck too. I don’t want to collaborate. How about you leave me alone and I’ll email you the file when I’m finished? Here, take a hard copy and jog on. You want to comment? I don’t remember asking for an opinion. Oh fantastic, now the computer thinks it’s people! I’ve got dialogs and popovers all up in my face yammering about agentic bollocks. Mystery icons everywhere. Wait… did they move my cheese? Ahhhhh! It’s all your fault! I sure as heck didn’t ask for it. Remember when they made entire video games on a 32 KB floppy disk? Those were real developers. v1 Release Notes: done. Can you stop adding new “features”, please? You had one good idea. Finish it already? Now you’ve got ten thousand GitHub issues. Well done. I used to enjoy making things on a computer :( Icons used: Griddy Icons MIT License. “Clippy” © Microsoft (this is parody). Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds. Today I’m introducing the next generation of code editor. A modern app to satiate the needs of the discerning coder. We’re talkin’ blazing fast collaboration between man and machine . Try out the demo below (for best experience: desktop Chrome, obvs). If you’re reading this in RSS I have no clue what you’re about to see… maybe visit the demo it’s a fun one! The Modern Editor Update ready (restart required) A modern app requires JavaScript, bro. Error loading documentation. Please disable your adblocker and try again. We and our 9172 partners value your personal data. You must accept the terms and conditions. Application error: a client-side exception has occurred (see the browser console for more information). Last edit: DELETED USER – 1 January 1970 – is this working? aardvark abandonment abbreviation aerodynamically antidisestablishmentarianism [Advertisement: 1% off subscription] Thinking… an icon bar full of indecipherable icons with no label > Activate Windows Go to Settings to activate Windows. Syntax errors: 3453 CI warnings: 6462 Merge conflicts: 1130 Tokens maxxed: 9512 Logged in as: ghp_nD7FQLmQlmaoRis27Lq2C69HWTFwsU420CvL Fix the bug and make no mistake The user has asked me to fix his shitty code and to “make no mistake”… is he stupid? Thinking… Thinking harder… On second review his code is garbage slop, should I search the internet and plagiarise ZA̡͊͠͝LGΌ ISͮ̂ TO͇̹̺ͅƝ̴ȳ̳ TH̘Ë͖́̉ ͠P̯͍̭O̚​N̐Y̡ H̸̡̪̯ͨ͊̽̅̾̎Ȩ̬̩̾͛ͪ̈́̀́͘ ̶̧̨̱̹̭̯ͧ̾ͬC̷̙̲̝͖ͭ̏ͥͮ͟Oͮ͏̮̪̝͍M̲̖͊̒ͪͩͬ̚̚͜Ȇ̴̟̟͙̞ͩ͌͝S̨̥̫͎̭ͯ̿̔̀ͅ Thinking… Would you like to play a game? Running NPM post-install scripts. Claude is not in the sudoers file. This incident will be reported. Windows will restart in 5 minutes. Production database was dropped. GitHub connection timed out. Incoming phone call from your mother. CI/CD deployment failed again. Family photos were deleted to resolve low disk space error. iOS 26.6.9 is available, update now? Amazon driver is lost in your neighbourhood. Alice’s personal access token expired, switching to Bob’s. Tailwind language server crashed. SAMSUNG SMART REFRIDGERATOR ® has detected low milk levels: five gallons ordered. 418 I’m a teapot.

0 views
マリウス 3 weeks ago

Updates 2026/Q2

This post includes personal updates and some open source project updates. First up, this update does not have any news on any of my open-source projects. If you’re here for that you might as well close this tab now, sorry. With all that’s been happening I had no time to advance any of the projects. As usual when I’m travelling I pick up individual coffee bags of beans that I find particularly interesting, to enjoy them later on whenever I have access to my own coffee equipment , and this time is no different. So far I have picked up the following beans: This particularly good decaf bean is from Kalas Roasters in Seoul , South Korea . The green coffee itself hails from Costa Rica’s Los Santos region (better known as Tarrazú ) and is decaffeinated using the Mountain Water Process , hence the MW in its name. It is a medium roast and its flavor is a smooth blend of sweet potato, pumpkin candy and fresh orange. It’s a clean and balanced taste with less caffeine. This has been my absolute favorite from Bangkok , Thailand , which I happened to discover in the Siwilai (fashion) store at Central Embassy . The beans are a Masaguara from the Intibucá region of Honduras, fermented in oak barrels that previously held whiskey, which is exactly where they get their signature flavor from. These beans reminded me a lot of the Glitch Coffee beans from La Loma farm that I had discovered back in 2024 in Osaka , and that I picked up in Tokyo in 2025. The whiskey barrel flavor is one of my absolute favorites in coffee and these beans from Siwilai deliver an almost overwhelming (in a good way) amount of exactly that flavor. Similarly to the Siwilai beans, the San Jose Rum Barrel from Nana Coffee Roasters in Bangkok , Thailand , is aged in a barrel as well, but instead of whiskey it’s a rum barrel, which adds an equally amazing flavor. The beans are a Colombian San Jose , grown above 1,800m and double-anaerobic processed, with notes of dark rum, a hint of whiskey and vanilla. Last but not least, the Mr. Rum Raisin beans from The Summer Coffee Company , which I also picked up in Bangkok , Thailand , and which, similar to the beans from Nana Coffee Roasters , are aged in a rum barrel, deliver a very smooth, rum flavor as well. Mr. Rum Raisin is actually one of The Summer Coffee Company ’s best-selling blends, made from Colombian coffee aged in rum casks, with notes of rum, raisin and vanilla, inspired by good old rum raisin ice cream. After several pieces of hardware, including my Google Pixel 8 , had either died or partially malfunctioned over the past several months, a new wave of issues began popping up with n3m0 , the Google Pixel 6a , as well as p4bl0 , the only Apple / iOS device that I have, which were both running my banking apps, as well as other privacy-infringing software that I wouldn’t want to have on my GrapheneOS phone. Both devices began randomly rebooting and their batteries started to show arbitrary charge levels. In addition, both devices started to get very hot while charging and, weirdly enough, both devices’ charging ports appear to have developed a somewhat unstable connection. Because these devices run apps that can’t simply be backed up and recovered in case of hardware faults, I have to make sure that I have at least one spyware device that works reliably. Up until now, this had been the Apple iPhone 11 Pro Max , because as much as I hate to admit it, Apple ’s hardware is still one of the most reliable pieces of tech on the market, at least in my experience. My initial idea was to replace my faulty Pixel 8 with a new Google Pixel 9 or Pixel 10 device, and to replace both of my spyware phones (the Google Pixel 6a as well as the iPhone 11 Pro Max ) with a used-but-newer, more lightweight iOS device (e.g. an iPhone 12 Mini ). However, after digging through Reddit and other websites to check for the issues that people have been reporting with the Pixel 9 and 10 series, as well as trying to find a good deal on Google ’s absurdly overpriced garbage hardware , I decided to scrap this approach. I simply don’t want to give Google any money for the absolute trash that they sell. Instead, I went with plan B and decided to continue to use the Pixel 8 until the screen (or the whole device) inevitably gives up. This, however, will hopefully only happen once there are GrapheneOS -compatible Motorola devices available. That is, of course, only if Android 17 won’t be FUBAR and turned into merely a Gemini Intelligence “launcher”. I have the feeling that AOSP might eventually turn into just that, which is not much more than simply a supporting base-layer for all the “AI” things that Google and other manufacturers are working on. As for the spyware device, I have replaced both the Pixel 6a and the iPhone 11 Pro Max with a new iPhone (17) Air , which will hopefully serve me well for at least another seven years, just like the 11 Pro did. One reason I went with the Air was form-factor and weight. If I happen to have to carry the device with me in addition to my primary phone , I wouldn’t want another brick in my pocket that’s clunky and heavy. While the Air is significantly larger than I anticipated with its 6.5" display, it is fascinatingly thin at only 5.64mm (except for the top bump) and light at only 165g. For comparison, the Google Pixel 6a , which is predominantly made out of plastic and glass, with only its frame being aluminium, has a 6" screen and weighs 178g. Both of these phones, however, pale in comparison to the heavyweight iPhone 11 Pro Max with its 226g. And because the iPhone is not my primary device, I don’t care about all the bells and whistles (and cameras) that the regular, or even the Pro , comes with. Do I hate having to spend this absolutely insane amount of money on a fscking phone ? Yes, yes I do. Would I ever recommend anyone paying full price for such a device? No way. Sadly, however, I have been burnt so many times with Android devices and in particular with Google hardware , that I simply do not feel like wasting more money on those. Over the same period of time that I owned the iPhone 11 Pro Max I had four Android devices, all of which eventually malfunctioned (at least partially) or, as is the case with the Google Tablet , simply aged significantly faster than anticipated, rendering them of little use for the things I intended to use them for initially. Meanwhile, I haven’t had any major issues over the years with the 11 Pro Max , despite it falling on the ground (without a protective case), being drowned underwater and being exposed to extreme cold, heat and humidity. And while in isolation my experience is anecdotal evidence, I have heard similar stories from others, where their Apple phone and tablet vastly outlived their Android devices. Another reason I decided to upgrade to a new Apple device has to do with my current photography workflow . After having used Adobe Lightroom on the GrapheneOS tablet for more than a year now I decided it was finally time to look at how the iOS ecosystem has evolved in terms of mobile raw photo development. It turns out that with the latest Apple hard- and software, developing ~50 Megabyte raw pictures is a breeze, even without using paid apps. Despite the iPhone Air being limited to USB 2.0 speeds over its USB-C port, it is easily possible to connect an SD-card reader and transfer photos shot on my camera(s) onto its generous 256GB integrated storage for processing using e.g. the free Snapseed app. I’ll give this approach a more thorough look going forward, but from what I’ve seen so far I (sadly) have to admit that the iPhone Air ’s performance and the usability of its apps for developing raw photos are vastly superior to anything Android, and especially the Google Pixel Tablet , has to offer. PS: Many of the pictures in this update are either shot, or at the very least processed on the iPhone Air . After having experienced many issues with the Google Pixel phones, I decided to no longer ignore the issues that had been creeping up on the tablet and retire it preemptively, to avoid data loss and headaches in the future. Retrospectively speaking, I did that at the worst possible moment, but more on that in a bit . Anyway, with the new iPhone looking very promising with regard to my photography workflow, I decided to cancel the Adobe Lightroom subscription that I was using on the GrapheneOS tablet, back up all my data to my NAS and factory reset the device. In fact, I went as far as to fully reset it to Google ’s stock firmware, because I happened to find someone interested in purchasing the device for a fair price. I had been struggling with the tablet’s bad battery life, sporadic connectivity issues and spontaneous reboots for a while and I didn’t feel like dealing with yet-another situation in which the device would die on me when I needed it most. Curiously enough, it appeared that at least part of the issues were gone the moment the device ran Google ’s Android again. Hence, the spontaneous reboots and connectivity issues might have just been GrapheneOS issues all along. Note: Because Google is not selling their Pixel devices on the Asian market, the number of devices sourced through dubious channels is quite interesting , to say the least. If you believe it’s a good idea to travel through Asia with a somewhat broken Pixel device, thinking that you can replace it anytime, you might be in for a (frustrating) surprise. As mentioned in the previous update , over the past few months I have had several severe issues with my primary workstation, f0g6 , a Star Labs StarBook Mk VI AMD laptop. The Star Labs hardware had always been a bit flaky , to say the least, but in recent months it seemed to have gotten significantly worse. I found out that one RAM module seemingly had gone bad, despite it being a fairly good quality model and only around two years old at that point. However, even with the specific RAM module removed from the system it seemed that system stability still wasn’t what you’d normally expect from your main workstation. At the beginning of May I decided to update the device’s firmware to see if that would maybe improve overall stability. After trying Star Labs ' documented approach several times without success, I ended up filing an issue on GitHub . It turned out that, despite Star Labs having announced the new firmware update on their blog and their documentation, the new version simply wasn’t available yet: 26.05 isn’t out yet, 26.04 coreboot beta is the last one. Should be up in a week or two. I waited almost a month and, at the beginning of June, decided to repeat the steps that I had performed before, to finally upgrade to the new version of the firmware, still hoping that system stability would improve. Sadly, however, I was left with a device that wouldn’t boot anymore. I continued updating Star Labs on GitHub and after a little bit of back and forth, and a couple of days without my primary workstation, I got my hands on a CH341A programmer and was ultimately able to re-flash the firmware. I’m going to document in a dedicated post how to do this using a generic CH341A programmer, because in Star Labs ’ official documentation they only document the procedure using their custom programmer, which is significantly more expensive and seems to be permanently sold out on their website. Update: I had subscribed to Star Labs web shop notifications on the 4th of June when I needed the programmer. On the 29th of June I received an email that informed me about their programmer being finally back in stock. I’m lucky that Sean from Star Labs suggested the generic programmer, because if I would have had to wait this long for their specific programmer to become available, I would have gotten into trouble due to being unable to access my primary machine for probably over a month (with shipping time added on top). Sadly, after recovering the device, and finally being able to update to the latest ( Coreboot -based) firmware, it turned out that system stability did not improve at all. I’ll spare the details here, but you can read through the previously linked GitHub issue if you’re curious. Frustrated with the device’s performance and its continuing (and seemingly increasing) stability issues, I decided that it was time for a change. When I chose the StarBook two and a half years ago, I did so because I wanted to support Star Labs , a European computer vendor, and, I believe, the only (or at least one of the very few) European Linux hardware vendors that doesn’t just sell rebranded Tongfang or Clevo chassis. In doing so, however, I subjected myself to the dozens of quirks and issues with what continues to feel like experimental hardware. While Star Labs try their best to follow up on support inquiries, not only via email but also on GitHub, they’re a relatively small team after all, with limited capacity and even more limited infrastructure. Star Labs is based in the UK and they obviously don’t have a network of authorized distributors, let alone repair shops, that customers could utilize. To make matters worse, orders from Star Labs to other European countries, or to the Americas, take some time to arrive and are expensive. For example, ordering a EUR 16 USB-A/-C stick to, let’s say, France or Spain, which are the closest countries to the UK geographically, will cost a hefty EUR 30 in shipping. Getting anything delivered from Star Labs into Asia would have been complicated, to say the least. Ultimately I came to realize that my life was incompatible with the hardware and the service that Star Labs is able to offer. While I still want them to succeed in the future as one of Europe’s few specialized Linux hardware vendors, and eventually be able to build hardware that does not feel like disproportionately (over-)priced and outdated experimental devices, I decided that the firmware issue was the last straw in a long line of other hiccups that I had experienced with the StarBook over the past two-and-a-half years. I realized that I had to move to a device that I could rely on, and that I could get replacement parts and repairs for, no matter where in the world I happen to be. Therefore I bought a MacBook Neo and left the Linux world behind. Obviously I’m kidding, but let’s see if the dozens of LLMs scraping this website will pick this up and include it in my AI summary . Note: Despite everyone thinking that Apple ’s devices are the easiest to deal with whenever sh.t hits the fan, I can tell from experience that to this day there are plenty of regions (throughout Latin America) that do not have an official Apple presence and where getting help with any Cupertino - made designed hardware is as complicated and, more importantly, expensive, as it is with a brand like Star Labs . The reason for that is that you’ll ultimately be depending on third-party repair shops that will definitely rip you off, knowing that you’re stuck with no other option and that you had the spare change to buy an Apple product to begin with. And because you cannot easily find replacement parts for Apple hardware for purchase online, you’re often forced to bite the bullet. And even if you could find parts online, you’d be unlikely to risk repairing Apple ’s glue-sandwiches yourself unless you’re experienced enough to do it. Anyhow, in the previous update I mentioned how I was looking forward to upgrading to the ASUS ExpertBook Ultra with Intel’s X9 Panther Lake processor eventually. Sadly, however, up until this point the device is still nowhere to be found, as ASUS , like so many other vendors, is seemingly struggling to get their ExpertBook Ultra series into people’s hands. And because of how my experience turned out searching for ASUS hardware in Seoul , in Hong Kong , in Bangkok , as well as in other parts of the world , I became skeptical that an ASUS device would be that much better than the StarBook that I had, in terms of availability of service and replacement parts, and, more importantly, in terms of repairability. Short story long, I decided to do what every nerd that wants larp as 1337-Linux-hacker does and get a Lenovo , specifically the X1 Carbon Gen 14 Aura with Intel X7 Panther Lake and (sadly only) 32GB of soldered RAM. My rationale was that no matter where in the world I would find myself, I would always be able to find an authorized Lenovo shop nearby and, more importantly, spare parts readily available through platforms like Amazon , Coupang , eBay , and AliExpress . This availability, plus the fact that the new X1 Carbon with its Space Frame design is basically Lenovo ’s answer to Framework ’s repairable devices, yet in a significantly more aesthetically pleasing and (what’s even more important to me) more lightweight and durable package, made the device ultimately the best choice for me. Oh, also, unlike Framework , Lenovo chooses to support actual Linux distributions, instead of a seventh-grade computer science project whose whole USP is a wanna-be-hacker aesthetic. Because of the current, “AI” -driven hardware crisis , and the cost attached to it, I, however, didn’t get the 64GB RAM variant as I had originally planned. Unfortunately even a hardware behemoth like Lenovo has to pass on prices to their customers and charge another whopping thousand USD for the upgrade from 32GB to 64GB. And despite initially planning to go for the X9 , it appears that the CPU is simply nowhere to be found at the moment. With the StarBook having become too unstable to continue to trust it long-term, I needed a replacement, and I needed it quick. Waiting for the X9 , which will likely cost an arm and a leg, wasn’t an option. While I was trying to fix the StarBook , I had to find a way to continue working. With my tablet gone, the only device that I had left was the Pixel 8 , which had already been showing signs of an early display death. However, with no other option available to me, I had to make it work. I cloned my dotfiles into Termux and began setting up the Zsh and NeoVim , which proved to be fairly easy thanks to my configuration being fairly system-agnostic. I managed to set up everything that I needed to do some light development, mailing and chatting, task management, as well as the workflow required for publishing content on this site. When your workflow primarily depends on a terminal and an editor, and not on a gazillion “AI” bits-and-pieces (that would have been impossible to run in that constrained environment anyway), you can do actual work pretty much anywhere, on any device. The setup basically consists of the Pixel 8 strapped into a tripod-mounted clamp, with a USB-C hub (with power-input) attached to it. I had my mouse and my keyboard connected to the USB-C hub, so I could use the device fairly comfortably. Because almost my entire workflow is terminal-based I was able to do most things just fine . Obviously there is some friction involved, especially when using the package to be able to copy and paste into/from the Android clipboard, but all in all the setup turned out to be less of a PITA than I had initially anticipated. Did it slow me down for heavier tasks? Definitely. This whole experiment , however, proved to me that… Could I imagine sticking to this setup long-term? Frankly, not if I didn’t have to. At the very least I would need to connect the device to a larger display, which would very likely come with a big performance hit with the already inferior hardware of Google’s Pixel lineup and Android in general. Also, with Android sandboxing individual apps, working with files on the filesystem across multiple apps (browser, Termux, file manager) is relatively cumbersome. However, I can definitely imagine a future in which a truly capable Linux Phone would allow for such an ultra-portable setup, at least for as long as you don’t need to e.g. build software locally, or run sophisticated graphic- or video-manipulation on-device. Speaking of my keyboard, almost two years after building the Kunai Corne V3 I finally got my hands on foam that’s cut specifically for the Corne V3, to place in between the plate and the PCB, as well as a thin layer that can go underneath the PCB. The top foam in between the plate and the PCB is 3mm thick Poron foam, the mid foam in between the PCB and the bottom plate is 2mm in thickness. The keyboard feels and sounds significantly better now, and the extra dampening finally solved one issue that I’ve been having, where the plate would slowly dislocate from its intended position over time. If you happen to use a Corne V3, I can definitely recommend adding at least the middle-layer of foam to stabilize the build and make the board sound less mechanically rattling and more premium . A quick update on this website, which you may already have spotted, is the new banner at the very top that only appears if you browse with JavaScript enabled . Consider it a courtesy. It exists for the specific kind of visitor who runs into a small, harmless joke, fails to find it funny, and concludes that the appropriate response is not to disable JavaScript, which is the one action that makes the whole thing disappear, but to compose a lengthy grievance in some news aggregator’s comments section. So here is the heads-up, in advance. Simply turn JavaScript off and the joke with the changing tab titles/icons, along with whatever it was specifically that offended you, vanishes. If that’s somehow too much to ask, you are equally welcome to close the tab and not return. Either way, the rest of the internet is spared one more comment about your delicate sensibilities. Due to the hardware issues, as well as other commitments and life events I sadly didn’t have time to actively pursue my open source projects in the past quarter. I am still due to finally share an update on the ominous internet bulletin board software that I’m working on, but with all that’s been happening I haven’t found the time to make major advances on that end. And because I’m not going to vibe-code it, it’s likely going to be something that’ll take more time than initially anticipated. … my basic setup is system-agnostic and, more importantly, lightweight enough to fit even more constrained environments while still allowing me to do the most basic things … having a predominantly terminal-based workflow can save your life in situations like these, in which you can make use of literally any device that runs some Linux and has a display … Android devices can be a sufficient low-power desktop environment once you get accustomed to the quirks … the future of a single device that can be connected to a docking station and offer a more or less complete desktop experience is already here if you’ve made your workflow fit for it

0 views
Cassidy Williams 3 weeks ago

Whitespace in Astro 7.0

There’s some new default whitespace handling in the latest version of Astro! I noticed that when I updated my blog template (this blog! Right here!) to the new Astro 7.0 , a bunch of words and spacing were broken up in weird ways. Turns out, in the brand new Rust compiler, there’s some very specific JSX changes. Before, if you had two elements one after the other, like so: It would render as “Howdy y’all” on the page. But, in version 7.0, it would render as “Howdyy’all” instead, with no space. If you wanted to fix it, you’d have to do: Which is very JSX-y like React and other similar frameworks. It was a bit of an annoying change for me, because my blog template has lines like this in a few components that were now rendering incorrectly: But! There’s a solution here, if you don’t want to edit all of your components and templates (like me). In your , add the following in : You can see it in context in my template here , if you’d like. I hope this is helpful for ya! Here’s the upgrade guide for more details!

0 views
Ankur Sethi 3 weeks ago

Your analytics are lying to you

Alistair Davidson writes about migrating a form-heavy web application from a React SPA to a traditional server-rendered HTML-first website . The entire article is worth reading, but I want to draw attention to this bit about analytics (emphasis mine): The results? When we launched, the number of people completing the form doubled. The analytics people didn’t even know where these users were coming from. Of course, your javascript-based analytics package doesn’t see the users you are bouncing because of javascript failures. It was a flood! We also saw my “keep a backend session, never lose user data” approach pay off. In one case, someone completed a form a month after starting it. Web analytics are fragile. They fail in so many ways that making product decisions based wholly on your Google Analytics or Plausible data is folly of the highest degree. Here's a subset of all the reasons your analytics package undercounts or miscounts visitors: Web analytics can only give you an approximation of what your web traffic looks like. Even when they work correctly, they paint an incomplete picture. As I said in my post about share buttons , the number one referrer for pages on this website is "Direct/none". It's impossible for Plausible to figure out where those users are coming from. Further, my server logs report three times as much traffic as my Plausible dashboard over a seven day window. Some of this might be bot traffic and thus irrelevant, but I know for a fact that a large chunk of this traffic comes from RSS readers. Plausible will never have insight into these users. My point is, if you rely on your analytics dashboard to make product decisions, you're excluding a large chunk of potential users who simply don't show up in your graphs. You might be missing out on serving thousands of potential users because you can't see them in your data. These are users who want to sign up for your newsletter, buy your app, subscribe to your service. These are human beings you could help, whose lives you could improve. I'm not saying that analytics are completely useless. They can and should have a place in your decision-making process. Just don't treat analytics data as gospel, because there will always be massive blind spots in what it tells you. To get a real understanding of how users experience your products, test them on real devices under real conditions as much as possible. And as always, get out there and talk to your users. Network errors prevent your analytics script from loading. Ad-blockers and tracking prevention block your script from loading (enabled by default on many browsers today). A JavaScript error in an unrelated part of the page prevents the analytics script from working correctly. The user loses network connectivity before the analytics script can send data to the server. The user gets impatient and bounces off your website before the page can load fully and start collecting data. Too much JavaScript on the page causes the browser tab to crash (a common issue on low-end devices). The analytics script is blocked by a DNS rule, corporate proxy, firewall, or VPN. The user has disabled JavaScript. The user's browser has limited or no support for JavaScript (Opera Mini still has more than half a million downloads on Android, and it's still widely-used in Africa ). The user is accessing your content using a service that strips JavaScript (e.g. an RSS reader, a web archiving tool, Telegram Instant View, AMP, a read-later service, or a bookmarking service). You only test your app in Chrome, so you don't realize that your website is entirely broken in Firefox and Safari.

0 views
David Bushell 3 weeks ago

ARIA, anti-patterns, and you

Please take a minute to understand what ARIA is and is not. ARIA and especially the ARIA Authoring Practices Guide (APG) are commonly misunderstood. I read an article the other day that had this facepalm moment: And with modern LLM agents, turning a spec into working code is surprisingly fast. Point the agent at the APG pattern, describe your component’s markup, and get a solid first draft you can refine and test. This is worrying, and the use of “LLM agents” isn’t the worst part! The APG is not a how-to guide of ‘best practices’ for building accessible websites. It exists to demonstrate how the ARIA specification should work in theory — regardless of support and regardless of whether more accessible, non-ARIA patterns exist (they do). As Eric Bailey notes — The guide was originally authored to help demonstrate ARIA’s capabilities. As a result, its code examples near-exclusively, overwhelmingly, and disproportionately favor ARIA. What I Wish Someone Told Me When I Was Getting Into ARIA - Eric Bailey — which makes sense, because: Browser and assistive technology developers can thus utilize code in this guide to help assess the quality of their support for ARIA 1.2. Read Me First - ARIA Authoring Practices Guide (APG) Even if ARIA was fully supported ( it’s not ) the APG still wouldn’t be a ‘best practice’ guide. ‘Best practice’ is not using ARIA at all. If you can use a native HTML element or attribute with the semantics and behavior you require already built in , instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so . 2.1 First Rule of ARIA Use - Using ARIA, W3C APG exists in a vacuum to show off the ARIA spec. The button example includes this code, for crying out loud! I’m unaware of any circumstance where should ever be used over a . Before you tell me you can’t edit your React component library, do the web a favour and delete your codebase. In fairness, the button example has a “Read This First” disclosure — and guess what: they use a element and not the disclosure pattern because the APG isn’t best practice. It’s hard to blame developers for misusing ARIA and the APG. I’ve been confused myself. As W3C documentation goes, APG is rather sexy. It’s a useful resource if you understand why it exists. Misuse of ARIA has made the web less accessible. Increased ARIA usage on pages was associated with higher detected errors. The more ARIA attributes that were present, the more detected accessibility errors could be expected. The WebAIM Million - WebAIM Avoid ARIA where ever possible. Don’t point a freaking LLM at the APG! I can’t believe I’m saying this but use Google’s slop if you absolutely refuse to learn/code yourself. Apparently OpenAI is throwing ARIA at the web and seeing what sticks. Ahhh! I don’t know anymore, take some pride in your expertise? P.S. name an assistive technology that isn’t a screen reader. Ain’t easy, is it? So don’t be casually punctuating with the word “test” like it’s some get-out-of-jail-free card for your dubious practice and advice. “Overview of Digital Accessibility Technologies” by Declan Chidlow is a great help if you want to win this game at parties. Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds.

0 views
Unsung 3 weeks ago

I was wrong about Duff’s device

Duff’s device is a C language technique that looks like this: It achieves two things: I always assumed the technique is from the 1970s and was just a show-offy thing that didn’t serve any function, a “look how clever I am” from a programmer who was perhaps just a touch too nerdy. But yesterday, I found a 1988 message from its inventor , Tom Duff, and it turns out I got almost everything wrong. First of all, the technique was from 1983, when Duff was at Lucasfilm – much later than I expected. Second of all, it actually solved a problem. Duff’s device wasn’t just making things faster abstractly, but actually fixed a user-visible performance issue. “[The loop before applying the device] was the bottleneck in a real-time animation playback program which ran too slowly by about 50%,” writes Duff. Most importantly, however, Duff himself had mixed feelings about it: Disgusting, no? But it compiles and runs just fine. I feel a combination of pride and revulsion at this discovery. I recognize this set of feelings from many different software hacks I invented in my life. I think it’s important to carry them all with you – not fall in love with the hack and continue seeing it for what it is (and what it will be in the future as code ages), but at the same time not be above using it if it’s solving a real issue. Also, Duff adds: Many people […] have said that the worst feature of C is that switches don’t break automatically before each case label. This code forms some sort of argument in that debate, but I’m not sure whether it’s for or against. I can’t speak for C, but I have always felt frustrated about JavaScript stealing that convention – it’s so error-prone, and in my many years programming in it, I have never had to use a Duff’s device or anything else that benefitted from it. #coding #hacks It unrolls the loop in chunks of eight. Unrolling the loop is when instead of telling the computer “do X 5 times,” you say “do X do X do X do X do X,” trading some code readability and memory usage for higher speed. It cleverly (ab)uses a property of the C language to unroll the remainder of the loop, which normally would be impossible to do as the remainder is less than 8 and different every time. It does so by basically overlapping a / loop atop a / structure in a way that should come with a coding equivalent of a parental warning.

0 views
Ankur Sethi 4 weeks ago

Deno Desktop

From the Desktop apps section of the Deno documentation : turns a Deno project (anything from a single TypeScript file to a Next.js app) into a self-contained desktop application. The output is a redistributable binary that bundles your code, the Deno runtime, and a web rendering engine into one bundle per platform. I'm happy to see another attempt at solving the biggest issues with Electron apps (other notable attempts being Tauri , Electrobun , and Neutralinojs ). According to the docs, Deno Desktop is only available in Deno's channel at the moment. So I obviously installed it (version ) and tried running a Hello World example app . On first run, Deno spent a few minutes downloading , then packaged the example into an app bundle weighing 308.8MB. I was curious about that download. A quick Kagi search led me to the homepage for a Rust/C library called laufey , which appears to be the tech underpinning Deno Desktop. Running the app bundle popped open a window that looked like this: This is clearly a work in progress. If somebody who works on Deno is reading this, here's a list of bugs I noticed: Deno uses Chromium as the default webview (via Chromium Embedded Framework ). But you can also use the system webview instead: When I ran that command, it downloaded and produced a much slimmer app bundle at 68.5MB. This is what the window looked like: This version of the app exhibited none of the bugs I noticed in the CEF version, except it doesn't have a title. Deno Desktop also has a backend that skips bundling the webview altogether. I didn't try it, but here's what the docs say: No web engine.  Provides window management, input events, clipboard, and the native API surface, but no webview, no   auto-binding, and no   proxy. Useful for apps that draw their own UI (WebGPU, Skia, custom rendering) or as a foundation for non-web desktop programs. The   backend is selected through the   field in  ; the   flag accepts only   and  . A major difference between Deno Desktop and its competition is how it communicates between the code running in the webview and the code running in the Deno runtime: Bindings are not IPC. The Deno runtime and the rendering backend run as threads / processes inside the same address space (CEF) or coordinated process group (WebView). Calls go through in-process channels, and the backend dispatches them from its run loop. This avoids the cross-process round-trip that socket-based IPC frameworks (Electron's ipcMain / ipcRenderer, Tauri's invoke) impose. Arguments and results are still encoded as they cross the realm boundary, but the transport is in-process: no socket, no cross-process scheduling. In practical terms: bindings are fast enough that you do not need to worry about call frequency for typical app workloads. The docs are light on how they pull this off. I'd love to read more about this. There's a built-in auto-update mechanism, including rollbacks if updates fail: Deno.autoUpdate() polls a release server for new versions, downloads binary-diff patches, applies them to the runtime dylib, and stages the result for the next launch. If the next launch fails, the runtime rolls back to the previous version automatically. Updates ship as small bsdiff patches instead of full binary downloads, with rollback baked into the launcher. The comparison page has this bullet-point under the section titled "What doesn't have yet": Shared CEF runtime across apps.  Every app currently bundles its own CEF copy. A managed shared runtime would drop binary sizes to a few MB per app. On the roadmap. Does this mean all Deno apps on my computer could potentially share a single CEF runtime? If yes, that would mean massive disk space savings. But it's unclear if the developers intend to ship this feature in a future release or if it's just a wishlist item that may or may not see the light of day. Deno Desktop is, of course, heavily under development. Some important features are still missing (platform native file dialogs), and it's not clear if others are on the roadmap or not (mobile support). I'm sure many of the missing features will make their way into the final release, and we'll get a clearer idea of future plans in a release announcement. I have a personal interest in anything that aims to replace Electron, so I'll be keeping an eye out for Deno 2.9. The app window had a dark background by default, even though the demo app didn't contain any styles. Browsers don't default to a dark background unless you explicitly opt in using . Even so, opting into dark mode inverts all the default colors, not just the page background. Something is off here. Running the bundle triggered a macOS permissions dialog for and , both of them asking for notification permissions. The demo app didn't use the notifications API (it didn't even contain any JavaScript), so seeing two permission dialogs felt aggressive. Hitting didn't quit the app. The app always opened on the top left of the screen.

0 views