<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vishwas Prakash</title>
    <description>The latest articles on DEV Community by Vishwas Prakash (@vishwas3000).</description>
    <link>https://dev.to/vishwas3000</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4104368%2F405fa731-4179-4c2e-bd8e-00cf5d53b582.jpg</url>
      <title>DEV Community: Vishwas Prakash</title>
      <link>https://dev.to/vishwas3000</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vishwas3000"/>
    <language>en</language>
    <item>
      <title>I ported JPEG AI to Apple silicon—and the first benchmark was mostly startup</title>
      <dc:creator>Vishwas Prakash</dc:creator>
      <pubDate>Thu, 10 Sep 2026 13:06:55 +0000</pubDate>
      <link>https://dev.to/vishwas3000/i-ported-jpeg-ai-to-apple-silicon-and-the-first-benchmark-was-mostly-startup-2mdn</link>
      <guid>https://dev.to/vishwas3000/i-ported-jpeg-ai-to-apple-silicon-and-the-first-benchmark-was-mostly-startup-2mdn</guid>
      <description>&lt;p&gt;My first JPEG AI decode result took almost five seconds. That number looked&lt;br&gt;
terrible next to JPEG—until I measured what those seconds contained.&lt;/p&gt;

&lt;p&gt;Only about 0.26 seconds was the reference decoder's codec operation. Model&lt;br&gt;
loading took about 0.83 seconds. Roughly 3.68 seconds went to Python imports,&lt;br&gt;
CUDA initialization, process startup, and I/O.&lt;/p&gt;

&lt;p&gt;That distinction became the point of this project: compare JPEG and JPEG AI,&lt;br&gt;
but do it in a way that does not confuse neural inference with cold-start&lt;br&gt;
machinery. Then take the same codec path to Apple silicon using Core ML and a&lt;br&gt;
native entropy coder.&lt;/p&gt;

&lt;p&gt;The code, benchmark harness, results, and macOS app are in the&lt;br&gt;
&lt;a href="https://github.com/Vishwas3000/ai_compression" rel="noopener noreferrer"&gt;ai_compression repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What JPEG AI actually is
&lt;/h2&gt;

&lt;p&gt;JPEG AI is not “run a chatbot on a JPEG.” It is a learned image codec. A neural&lt;br&gt;
analysis transform replaces the hand-designed frequency transform and&lt;br&gt;
quantization pipeline familiar from conventional JPEG. A standards-defined&lt;br&gt;
bitstream still carries the result, and a decoder reconstructs pixels with a&lt;br&gt;
neural synthesis transform.&lt;/p&gt;

&lt;p&gt;The JPEG committee lists the core coding system as&lt;br&gt;
&lt;a href="https://jpeg.org/jpegai/workplan.html" rel="noopener noreferrer"&gt;ISO/IEC 6048-1:2025&lt;/a&gt;. Its&lt;br&gt;
&lt;a href="https://gitlab.com/wg1/jpeg-ai/jpeg-ai-reference-software" rel="noopener noreferrer"&gt;reference software&lt;/a&gt;&lt;br&gt;
contains encoder, decoder, training, and evaluation code. The implementation is&lt;br&gt;
available under its included BSD license, although that license explicitly does&lt;br&gt;
not grant patent rights. “Open source implementation” and “unrestricted codec&lt;br&gt;
patent rights” are different claims.&lt;/p&gt;
&lt;h2&gt;
  
  
  From pixels to &lt;code&gt;y&lt;/code&gt; and &lt;code&gt;z&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The simple-profile encoder used here follows this path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RGB pixels
   ↓ BT.709 YUV conversion
analysis transform
   ↓
y latent ──────────────┐
   ↓ hyper-encoder     │
z hyper-latent         │
   ↓ quantize + code   │
hyper decoders         │
   ↓ scale, mask, ψ    │
four-stage context model
   ↓ predict y + quantize residual
me-tANS entropy coder
   ↓
JPEG AI bitstream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;y&lt;/code&gt; is the main learned representation. For the luma model in this test it has&lt;br&gt;
160 channels at roughly one-sixteenth the input width and height. It retains&lt;br&gt;
spatial content, but its channels are not red, green, blue, or named features.&lt;br&gt;
The network learns whatever representation minimizes its training objective.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;z&lt;/code&gt; is a lower-resolution hyper-latent, roughly one-sixty-fourth the input&lt;br&gt;
dimensions. It describes statistics that help the decoder predict and entropy&lt;br&gt;
code &lt;code&gt;y&lt;/code&gt;: which values are likely, which positions should be coded, and how to&lt;br&gt;
reconstruct the latent in context. Spending a small number of bits on &lt;code&gt;z&lt;/code&gt; can&lt;br&gt;
make the much larger &lt;code&gt;y&lt;/code&gt; stream cheaper.&lt;/p&gt;

&lt;p&gt;Both traditional JPEG and JPEG AI eventually use entropy coding. The learned&lt;br&gt;
codec's interesting advantage is upstream: it jointly learns the representation&lt;br&gt;
to encode and a richer conditional probability model for that representation.&lt;br&gt;
In simplified form, it estimates &lt;code&gt;P(yᵢ | z, context)&lt;/code&gt;; a sharper estimate gives&lt;br&gt;
the entropy coder more predictable symbols and therefore shorter codes. This is&lt;br&gt;
learned versus hand-designed modeling, not random versus deterministic decoding.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcfcbffteb3vt87bbj1u0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcfcbffteb3vt87bbj1u0.png" alt="Input luma, y activation energy, and z activation energy captured during one Core ML encode" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are real tensors captured at Core ML model boundaries. The &lt;code&gt;y&lt;/code&gt; energy&lt;br&gt;
map still resembles the bird and its surroundings. The &lt;code&gt;z&lt;/code&gt; map is much coarser&lt;br&gt;
and sparser. That is expected: it describes coding context, not a thumbnail.&lt;/p&gt;

&lt;p&gt;The app can also export every one of the 160 luma &lt;code&gt;y&lt;/code&gt; channels, every rounded&lt;br&gt;
&lt;code&gt;z&lt;/code&gt; channel, and entropy-mask density. Orange and blue in the channel sheets&lt;br&gt;
mean positive and negative activation. Each channel is normalized separately,&lt;br&gt;
so those colors must not be interpreted as directly comparable magnitudes.&lt;/p&gt;

&lt;p&gt;For the fixed network topology, I use &lt;a href="https://netron.app/" rel="noopener noreferrer"&gt;Netron&lt;/a&gt; to open an&lt;br&gt;
ONNX file such as &lt;code&gt;apple/Models/onnx/tools_2/model_y/analysis.onnx&lt;/code&gt;. Netron shows&lt;br&gt;
operators, weights, shapes, and connections. It does not show the values from a&lt;br&gt;
particular inference. The app's tensor export provides that second half.&lt;/p&gt;
&lt;h2&gt;
  
  
  How the model is trained
&lt;/h2&gt;

&lt;p&gt;Training and inference are separate phases. During training, the encoder and&lt;br&gt;
decoder weights are optimized against a rate-distortion objective commonly&lt;br&gt;
written as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;loss = distortion + β × rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Distortion penalizes reconstruction error. Rate estimates the bits needed for&lt;br&gt;
the latents. A larger &lt;code&gt;β&lt;/code&gt; makes bits more expensive; a smaller one allows a&lt;br&gt;
larger stream to preserve more detail.&lt;/p&gt;

&lt;p&gt;The reference recipe trains four base rate-distortion models with beta values&lt;br&gt;
0.002, 0.012, 0.075, and 0.5. Gain parameters and a beta displacement expose&lt;br&gt;
intermediate operating points without shipping a completely separate network&lt;br&gt;
for every bitrate.&lt;/p&gt;

&lt;p&gt;The current reference documentation describes 132 optimization epochs per&lt;br&gt;
base model, followed by a statistics stage. It progresses from fixed-rate MSE&lt;br&gt;
training, through mixed MSE/MS-SSIM distortion, to decoder-side variable-rate&lt;br&gt;
training. The last pass records activation-clipping statistics needed by the&lt;br&gt;
codec. See the official&lt;br&gt;
&lt;a href="https://gitlab.com/wg1/jpeg-ai/jpeg-ai-reference-software/-/blob/main/docs/architecture/13-training.md" rel="noopener noreferrer"&gt;training recipe&lt;/a&gt;&lt;br&gt;
and &lt;a href="https://gitlab.com/wg1/jpeg-ai/jpeg-ai-reference-software/-/blob/main/docs/architecture/08-neural-network-components.md" rel="noopener noreferrer"&gt;network component guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;During ordinary encoding or decoding, none of that training happens. The&lt;br&gt;
weights are frozen. Inference is a sequence of tensor transforms plus&lt;br&gt;
quantization and entropy coding.&lt;/p&gt;

&lt;p&gt;That also means JPEG AI does not silently improve as people encode more photos.&lt;br&gt;
Per-image probability adaptation is different from changing network weights.&lt;br&gt;
A future domain-specific or continually trained codec could specialize for&lt;br&gt;
medical images, screenshots, satellite imagery, or one camera fleet, but the&lt;br&gt;
encoder and decoder would then need matching model versions. Model distribution,&lt;br&gt;
update bandwidth, privacy, out-of-distribution behavior, and the cost of sending&lt;br&gt;
weights could erase the saved image bits. Federated or personalized codec&lt;br&gt;
training is a real research direction, not behavior provided by today's&lt;br&gt;
standard decoder.&lt;/p&gt;
&lt;h2&gt;
  
  
  The first rate-distortion result
&lt;/h2&gt;

&lt;p&gt;I ran conventional JPEG at qualities 30, 50, 70, and 90, and JPEG AI at five&lt;br&gt;
reference operating points. The source was one 560×888 image from the JPEG AI&lt;br&gt;
test set.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fth9ge045arkwvzbhvnbm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fth9ge045arkwvzbhvnbm.png" alt="Rate-distortion plot for JPEG and JPEG AI on one test image" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Near the same rate, JPEG quality 70 produced 1.081 bits per pixel and 34.55 dB&lt;br&gt;
RGB PSNR. JPEG AI point 75 produced 1.092 bits per pixel and 37.04 dB: a 2.49 dB&lt;br&gt;
advantage on this image.&lt;/p&gt;

&lt;p&gt;The same pair now also measures 0.9275 versus 0.9536 SSIM-Y, 0.9926 versus&lt;br&gt;
0.9962 MS-SSIM-Y, and 0.0756 versus 0.0680 LPIPS-Alex. Higher is better for&lt;br&gt;
SSIM; lower is better for LPIPS. All three move in JPEG AI's favor here, but&lt;br&gt;
they are still observations from one image rather than population estimates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgrire0aurr72mvdkg8dh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgrire0aurr72mvdkg8dh.png" alt="Original, JPEG, and JPEG AI detail crops at nearly equal rates" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a smoke result, not a codec-wide conclusion. It has one image, one&lt;br&gt;
measurement per point, no warmup, and GPU contention on the remote machine.&lt;br&gt;
The JPEG baseline also used 4:2:0 chroma while this JPEG AI path used 4:4:4.&lt;br&gt;
A publishable study needs a fixed multi-image corpus, repeated uncontended&lt;br&gt;
runs, confidence intervals, rate-distortion integration such as BD-rate, and&lt;br&gt;
ideally a controlled subjective test. Metrics do not replace looking at the&lt;br&gt;
images.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the first timing looked so high
&lt;/h2&gt;

&lt;p&gt;The benchmark preserves total wall time and parses timers printed by the&lt;br&gt;
official reference implementation. That made the hidden startup cost visible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7m4p15qncibbse80druh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7m4p15qncibbse80druh.png" alt="JPEG AI encode and decode timing split into codec, model loading, and process overhead" width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Across the five operating points, the component medians were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Codec operation&lt;/th&gt;
&lt;th&gt;Model loading&lt;/th&gt;
&lt;th&gt;Python/CUDA/process/I/O&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Encode&lt;/td&gt;
&lt;td&gt;1.07 s&lt;/td&gt;
&lt;td&gt;0.87 s&lt;/td&gt;
&lt;td&gt;3.73 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decode&lt;/td&gt;
&lt;td&gt;0.26 s&lt;/td&gt;
&lt;td&gt;0.83 s&lt;/td&gt;
&lt;td&gt;3.68 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Starting a new Python process for every image is valid end-to-end latency, but&lt;br&gt;
it is not steady-state codec throughput. A long-running service, application,&lt;br&gt;
or hardware decoder loads models once and amortizes that cost. Both numbers&lt;br&gt;
matter, so the CSV keeps them in separate columns rather than deleting the cold&lt;br&gt;
start.&lt;/p&gt;
&lt;h2&gt;
  
  
  Moving the path to Apple silicon
&lt;/h2&gt;

&lt;p&gt;The native implementation converts the reference ONNX graphs to Core ML and&lt;br&gt;
runs them with Apple's compute stack. Swift handles image conversion,&lt;br&gt;
bitstream structure, model orchestration, and reconstruction. A small C++&lt;br&gt;
module ports the reference me-tANS entropy encoder/decoder where exact integer&lt;br&gt;
state transitions matter.&lt;/p&gt;

&lt;p&gt;The integer hyper-scale decoder was the sharpest edge. Its output selects an&lt;br&gt;
entropy distribution. A one-index mismatch does not create a slightly different&lt;br&gt;
pixel; it can desynchronize entropy decoding. That stage therefore has to match&lt;br&gt;
the reference exactly, while small floating-point differences in synthesis can&lt;br&gt;
be evaluated with numerical tolerances.&lt;/p&gt;

&lt;p&gt;The native encoder produced a 63,599-byte model-2 stream for the test image:&lt;br&gt;
1.023 bits per pixel and 36.51 dB against the source. The official PyTorch&lt;br&gt;
decoder accepted it, and the hyper-latent, scale, mask, and residual control&lt;br&gt;
hashes matched. That verifies bitstream interoperability through entropy&lt;br&gt;
decoding.&lt;/p&gt;

&lt;p&gt;The native and official reconstructed PNGs measured 58.37 dB against each&lt;br&gt;
other. Their remaining difference is concentrated at the bottom edge, where&lt;br&gt;
the exported synthesis graph does not yet reproduce the reference decoder's&lt;br&gt;
runtime crop. This is an interoperability milestone, not complete pixel-exact&lt;br&gt;
conformance.&lt;/p&gt;

&lt;p&gt;On the Mac, the first native encode took 1.63 seconds and a later encode in the&lt;br&gt;
same process took 0.86 seconds. Those are still provisional single-image&lt;br&gt;
figures, but they show why keeping a model cache inside the app matters.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try the macOS app and inspect an inference
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;apple/Package.swift&lt;/code&gt; in Xcode, select the &lt;code&gt;JPEGAIDecoder&lt;/code&gt; scheme and &lt;strong&gt;My&lt;br&gt;
Mac&lt;/strong&gt;, then press Run. Or build the self-contained local app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;apple
./build_macos_app.sh
open dist/JPEGAIDecoder.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose &lt;strong&gt;Encode PNG&lt;/strong&gt;, select a rate point, and leave &lt;strong&gt;Export y/z inference&lt;br&gt;
visualizations&lt;/strong&gt; enabled. After the encode, choose &lt;strong&gt;View Inference Steps&lt;/strong&gt;.&lt;br&gt;
The folder is numbered in pipeline order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;input luma;&lt;/li&gt;
&lt;li&gt;all &lt;code&gt;y&lt;/code&gt; channels;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;y&lt;/code&gt; activation energy;&lt;/li&gt;
&lt;li&gt;all quantized &lt;code&gt;z&lt;/code&gt; channels;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;z&lt;/code&gt; activation energy;&lt;/li&gt;
&lt;li&gt;entropy-mask density;&lt;/li&gt;
&lt;li&gt;quantized &lt;code&gt;y&lt;/code&gt; residual energy after context prediction.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The app also performs a verification decode, displays the reconstruction, and&lt;br&gt;
labels the timing as a first or repeat run for that model. It separately reports&lt;br&gt;
the source PNG, the JPEG AI &lt;code&gt;.bits&lt;/code&gt; payload, a native macOS JPEG quality-0.70&lt;br&gt;
baseline, and the decoded PNG. The last file is a lossless preview of the&lt;br&gt;
reconstructed pixels, so it can be larger than both the source PNG and the&lt;br&gt;
compressed payload without indicating a compression failure.&lt;/p&gt;

&lt;p&gt;For a quicker command-line trial on an Apple-silicon Mac running macOS 13 or&lt;br&gt;
newer, the experimental encoder and decoder are now available from the&lt;br&gt;
project's Homebrew tap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;vishwas3000/tap/jpeg-ai-apple
jpeg-ai encode input.png output.bits &lt;span class="nt"&gt;--preset&lt;/span&gt; 75 &lt;span class="nt"&gt;--visualizations&lt;/span&gt; steps
jpeg-ai decode output.bits reconstructed.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The formula builds the native Swift package and installs the converted Core ML&lt;br&gt;
models. The supported presets are &lt;code&gt;12&lt;/code&gt;, &lt;code&gt;25&lt;/code&gt;, &lt;code&gt;50&lt;/code&gt;, &lt;code&gt;75&lt;/code&gt;, and &lt;code&gt;100&lt;/code&gt;. This is&lt;br&gt;
still an untiled 4:4:4 simple-profile experiment, not a general JPEG AI decoder&lt;br&gt;
or a claim of full conformance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Could this become a video codec?
&lt;/h2&gt;

&lt;p&gt;Encoding every frame independently would work today, just as Motion JPEG does,&lt;br&gt;
but it would waste temporal redundancy. A competitive video codec needs motion&lt;br&gt;
estimation or learned temporal prediction, reference-frame management, random&lt;br&gt;
access, rate control, error resilience, and hardware-friendly scheduling. The&lt;br&gt;
image model is useful research input, not a drop-in video standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this could go next
&lt;/h2&gt;

&lt;p&gt;The immediate engineering work is measurable rather than speculative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;make native reconstruction bit-exact at every boundary, including the
remaining bottom-edge crop;&lt;/li&gt;
&lt;li&gt;persist compiled models and add a batch mode so the command-line path does
not repay model compilation and loading for every image;&lt;/li&gt;
&lt;li&gt;compare Core ML's &lt;code&gt;.all&lt;/code&gt;, &lt;code&gt;.cpuAndGPU&lt;/code&gt;, and &lt;code&gt;.cpuAndNeuralEngine&lt;/code&gt; policies.
The current &lt;code&gt;.cpuAndGPU&lt;/code&gt; setting explicitly excludes the Neural Engine, but
only device measurements can say which policy wins;&lt;/li&gt;
&lt;li&gt;remove &lt;code&gt;[Float32]&lt;/code&gt;/&lt;code&gt;MLMultiArray&lt;/code&gt; copies with reusable Float16 or
IOSurface-backed buffers and keep PNG export outside the decode timer;&lt;/li&gt;
&lt;li&gt;benchmark a fixed corpus with warm and cold latency, peak memory, model size,
energy per image, PSNR, SSIM, MS-SSIM, LPIPS, and BD-rate;&lt;/li&gt;
&lt;li&gt;use Instruments signposts to separate Core ML, entropy coding, color
conversion, and file I/O on Apple silicon;&lt;/li&gt;
&lt;li&gt;test 8-bit weight quantization or palettization, accepting it only if
bitstream interoperability and the full rate-distortion corpus still pass;&lt;/li&gt;
&lt;li&gt;bring the same decoder to iPhone and compare CPU, GPU, and Neural Engine
scheduling without changing the standards-defined result; and&lt;/li&gt;
&lt;li&gt;run the published profiles, file format, and conformance material as those
pieces mature, rather than treating one reference configuration as the whole
standard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those foundations hold, the interesting applications extend beyond making&lt;br&gt;
smaller photos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CDNs and cloud photo libraries:&lt;/strong&gt; lower transfer and storage at a chosen
visual quality, if decode cost is amortized or hardware-accelerated;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;low-bandwidth capture:&lt;/strong&gt; cameras, drones, and remote sensing can spend bits
where detail matters, including regions of interest;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;on-device media:&lt;/strong&gt; a standard mobile decoder could make learned compression
interoperable instead of tying every app to its own model and bitstream;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;machine vision without full pixel reconstruction:&lt;/strong&gt; the same compressed
latent representation can feed downstream analysis. JPEG reported an
earth-observation segmentation experiment using nearly ten times fewer model
parameters, which is a particularly compelling direction for edge systems;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;domain-specific codecs:&lt;/strong&gt; controlled model versions could target medical,
satellite, scientific, or screen content whose statistics differ from the
general training set; and&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;privacy-preserving fleet improvement:&lt;/strong&gt; federated retraining could improve a
later codec model without collecting every original centrally, provided model
synchronization and privacy claims are tested rather than assumed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The highest-value development may therefore be a shared representation for&lt;br&gt;
both people and machines, not merely a better &lt;code&gt;.jpg&lt;/code&gt;. That claim needs task&lt;br&gt;
accuracy, power, and latency measurements alongside image quality.&lt;/p&gt;

&lt;p&gt;There is no single universally superior codec. If &lt;code&gt;.jpg&lt;/code&gt; compatibility is&lt;br&gt;
mandatory, &lt;a href="https://github.com/google/jpegli" rel="noopener noreferrer"&gt;Jpegli&lt;/a&gt; is a lower-friction&lt;br&gt;
baseline. &lt;a href="https://jpeg.org/jpegxl/" rel="noopener noreferrer"&gt;JPEG XL&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://aomedia.org/specifications/avif/" rel="noopener noreferrer"&gt;AVIF&lt;/a&gt; are mature modern-image&lt;br&gt;
comparators. &lt;a href="https://jpeg.org/jpegxs/" rel="noopener noreferrer"&gt;JPEG XS&lt;/a&gt; targets visually lossless,&lt;br&gt;
line-scale latency rather than maximum distribution compression. For video,&lt;br&gt;
&lt;a href="https://av2.aomedia.org/v1.0.0/20260528_38f28e7_AV2_Spec_v1.0.0.pdf" rel="noopener noreferrer"&gt;AV2 v1.0&lt;/a&gt;&lt;br&gt;
is a new conventional standard, while learned systems such as&lt;br&gt;
&lt;a href="https://openaccess.thecvf.com/content/WACV2024/html/van_Rozendaal_MobileNVC_Real-Time_1080p_Neural_Video_Compression_on_a_Mobile_Device_WACV_2024_paper.html" rel="noopener noreferrer"&gt;MobileNVC&lt;/a&gt;&lt;br&gt;
and&lt;br&gt;
&lt;a href="https://openaccess.thecvf.com/content/CVPR2025/html/Jia_Towards_Practical_Real-Time_Neural_Video_Compression_CVPR_2025_paper.html" rel="noopener noreferrer"&gt;DCVC-RT&lt;/a&gt;&lt;br&gt;
show that mobile and real-time neural video are plausible research directions.&lt;br&gt;
The next benchmark should include JPEG, Jpegli, JPEG XL, and AVIF instead of&lt;br&gt;
trying to infer a universal winner from JPEG alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Could JPEG AI become mainstream?
&lt;/h2&gt;

&lt;p&gt;It has two ingredients many learned-codec experiments lack: a published&lt;br&gt;
standards family and a reference bitstream implementation. That gives software&lt;br&gt;
and hardware vendors a common target.&lt;/p&gt;

&lt;p&gt;JPEG AI is now the first published international image coding standard built&lt;br&gt;
around an end-to-end learned codec, not a laboratory proposal. The core system&lt;br&gt;
was published in 2025, while profiling and a file format followed in 2026;&lt;br&gt;
reference-software and conformance work are still moving through publication.&lt;/p&gt;

&lt;p&gt;Mainstream adoption still requires fast low-power decoders, stable conformance&lt;br&gt;
tests, manageable model storage, browser and operating-system support, creation&lt;br&gt;
tools, and clear licensing decisions. Conventional JPEG wins today because it&lt;br&gt;
is everywhere and essentially free to decode. JPEG AI must offer enough quality&lt;br&gt;
or functionality to pay for a much more complex decoder.&lt;/p&gt;

&lt;p&gt;The next work in this repository is deliberately less glamorous: repair the&lt;br&gt;
remaining crop difference, benchmark a proper corpus with warm caches and no&lt;br&gt;
contention, add energy and memory measurements, add an iOS target, and only&lt;br&gt;
then prepare an upstream contribution and public launch post.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;JPEG Committee, &lt;a href="https://jpeg.org/jpegai/" rel="noopener noreferrer"&gt;JPEG AI overview&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;ISO, &lt;a href="https://www.iso.org/standard/88911.html" rel="noopener noreferrer"&gt;ISO/IEC 6048-1:2025 — JPEG AI core coding
system&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;JPEG Committee, &lt;a href="https://jpeg.org/jpegai/workplan.html" rel="noopener noreferrer"&gt;JPEG AI workplan and published
specifications&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;JPEG Committee, &lt;a href="https://gitlab.com/wg1/jpeg-ai/jpeg-ai-reference-software" rel="noopener noreferrer"&gt;official JPEG AI reference
software&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Esenlik et al., &lt;a href="https://doi.org/10.1109/TCSVT.2025.3613244" rel="noopener noreferrer"&gt;“An Overview of the JPEG AI Learning-Based Image Coding
Standard”&lt;/a&gt;, IEEE Transactions on
Circuits and Systems for Video Technology, 2026.&lt;/li&gt;
&lt;li&gt;Reference software documentation, &lt;a href="https://gitlab.com/wg1/jpeg-ai/jpeg-ai-reference-software/-/blob/main/docs/architecture/13-training.md" rel="noopener noreferrer"&gt;training
recipe&lt;/a&gt;
and &lt;a href="https://gitlab.com/wg1/jpeg-ai/jpeg-ai-reference-software/-/blob/main/docs/architecture/08-neural-network-components.md" rel="noopener noreferrer"&gt;neural-network component
guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;JPEG Committee, &lt;a href="https://jpeg.org/items/20260608_press.html" rel="noopener noreferrer"&gt;April 2026 implementation and compressed-domain inference
update&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;JPEG Committee, &lt;a href="https://jpeg.org/items/20260328_press.html" rel="noopener noreferrer"&gt;January 2026 mobile, video, and energy
roadmap&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;JPEG Committee, &lt;a href="https://jpeg.org/jpegai/dataset.html" rel="noopener noreferrer"&gt;CC0 JPEG AI test dataset&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Apple, &lt;a href="https://developer.apple.com/documentation/coreml/mlcomputeunits" rel="noopener noreferrer"&gt;Core ML compute-unit
options&lt;/a&gt; and
&lt;a href="https://developer.apple.com/videos/play/wwdc2022/10027/" rel="noopener noreferrer"&gt;performance guidance&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Full production and competing-codec research is maintained in the
&lt;a href="https://github.com/Vishwas3000/ai_compression/blob/main/PRODUCTION_RESEARCH.md" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>performance</category>
      <category>macos</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The .gitignore pattern that stopped matching when I moved a folder</title>
      <dc:creator>Vishwas Prakash</dc:creator>
      <pubDate>Wed, 02 Sep 2026 09:54:14 +0000</pubDate>
      <link>https://dev.to/vishwas3000/the-gitignore-pattern-that-stopped-matching-when-i-moved-a-folder-279p</link>
      <guid>https://dev.to/vishwas3000/the-gitignore-pattern-that-stopped-matching-when-i-moved-a-folder-279p</guid>
      <description>&lt;p&gt;I moved a directory this week and quietly un-ignored a file containing live&lt;br&gt;
credentials. Nothing failed. Nothing warned. &lt;code&gt;git status&lt;/code&gt; simply began offering&lt;br&gt;
to commit a file it had been hiding for months, and if I had run &lt;code&gt;git add -A&lt;/code&gt;&lt;br&gt;
without looking, the Supabase URL and key would have gone to a remote.&lt;/p&gt;

&lt;p&gt;The cause is one sentence of the gitignore spec that I knew, had never had to&lt;br&gt;
think about, and got caught by anyway.&lt;/p&gt;
&lt;h2&gt;
  
  
  The move
&lt;/h2&gt;

&lt;p&gt;The repository holds a Swift app. An Android app arrived, so the top level got&lt;br&gt;
lopsided — one platform in a folder, the other spread across &lt;code&gt;App/&lt;/code&gt;, &lt;code&gt;Checks/&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;Support/&lt;/code&gt; and a couple of scripts at the root. I moved the Swift half under&lt;br&gt;
&lt;code&gt;apple/&lt;/code&gt;, so &lt;code&gt;apple/&lt;/code&gt; and &lt;code&gt;android/&lt;/code&gt; sat side by side.&lt;/p&gt;

&lt;p&gt;Pure &lt;code&gt;git mv&lt;/code&gt;. No file contents changed. All the checks passed, both platforms&lt;br&gt;
built, and it looked like exactly the boring structural commit it was.&lt;/p&gt;

&lt;p&gt;Then, out of habit, I looked at what git was proposing to commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;?? apple/Support/pooled-flight/
?? apple/Support/telemetry.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of those had been ignored an hour earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Here is the relevant part of the &lt;code&gt;.gitignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;build&lt;/span&gt;/
*.&lt;span class="n"&gt;xcodeproj&lt;/span&gt;
&lt;span class="n"&gt;dist&lt;/span&gt;/
&lt;span class="n"&gt;Support&lt;/span&gt;/&lt;span class="n"&gt;telemetry&lt;/span&gt;.&lt;span class="n"&gt;env&lt;/span&gt;
&lt;span class="n"&gt;Support&lt;/span&gt;/&lt;span class="n"&gt;pooled&lt;/span&gt;-&lt;span class="n"&gt;flight&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five patterns. Three of them survived the move and two did not, and the&lt;br&gt;
difference is a slash:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a pattern contains a slash anywhere except at the end, it is treated as&lt;br&gt;
relative to the directory containing the &lt;code&gt;.gitignore&lt;/code&gt; file. Otherwise it&lt;br&gt;
matches at any depth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;build/&lt;/code&gt; has a slash only at the end, so it is a &lt;em&gt;name&lt;/em&gt; — it matches &lt;code&gt;build/&lt;/code&gt; at&lt;br&gt;
any depth, including &lt;code&gt;apple/build/&lt;/code&gt;. &lt;code&gt;*.xcodeproj&lt;/code&gt; has no slash at all, so it&lt;br&gt;
matches anywhere too.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Support/telemetry.env&lt;/code&gt; has a slash in the middle. That makes it &lt;strong&gt;anchored to&lt;br&gt;
the repository root&lt;/strong&gt;. It means &lt;code&gt;&amp;lt;root&amp;gt;/Support/telemetry.env&lt;/code&gt; and nothing else.&lt;br&gt;
The moment the file became &lt;code&gt;apple/Support/telemetry.env&lt;/code&gt;, the pattern stopped&lt;br&gt;
describing it, and the file became an ordinary untracked file that git was&lt;br&gt;
helpfully offering to add.&lt;/p&gt;

&lt;p&gt;This is correct behaviour. It is documented. It is also completely silent: there&lt;br&gt;
is no warning when a pattern matches nothing, because a pattern matching nothing&lt;br&gt;
is a normal state for most patterns most of the time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhkv03w53bzy22jxmt4hn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhkv03w53bzy22jxmt4hn.png" alt="Two repository trees. Before the move the pattern matches and the file is ignored; after a directory appears above it, the same pattern misses and the file is tracked." width="799" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Neither file moved relative to the other, and the &lt;code&gt;.gitignore&lt;/code&gt; was not edited. The tree grew a directory above them both, which is all it takes.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Checking, rather than assuming
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git check-ignore -v&lt;/code&gt; answers the question directly, and it names the pattern and&lt;br&gt;
line number that did the ignoring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git check-ignore &lt;span class="nt"&gt;-v&lt;/span&gt; apple/Support/telemetry.env
&lt;span class="gp"&gt;                                      #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;← no output. Not ignored.
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git check-ignore &lt;span class="nt"&gt;-v&lt;/span&gt; apple/build
&lt;span class="gp"&gt;.gitignore:1:build/ apple/build      #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;← still fine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty output means not ignored. That asymmetry — silence for the dangerous&lt;br&gt;
answer — is worth knowing before you rely on it.&lt;/p&gt;

&lt;p&gt;The other question worth asking immediately, and the one I was most anxious&lt;br&gt;
about, is whether the file had ever been committed on any branch at any point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'*telemetry.env'&lt;/span&gt;
&lt;span class="gp"&gt;                                      #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;← empty. Never committed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It had not. A near miss rather than an incident. If it had &lt;em&gt;not&lt;/em&gt; been empty, the&lt;br&gt;
fix is very different and much worse: rotate the credential first, then worry&lt;br&gt;
about rewriting history, in that order and never the other way round.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix, and why it is the fix
&lt;/h2&gt;

&lt;p&gt;The obvious repair is to re-point the patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple/Support/telemetry.env
apple/Support/pooled-flight/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works today and breaks the next time anything moves. It is the same bug,&lt;br&gt;
rearmed.&lt;/p&gt;

&lt;p&gt;The better repair is to stop anchoring them at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;telemetry.env
pooled-flight/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No slash in the middle, so they match by name at any depth, wherever the tree&lt;br&gt;
goes next. Slightly broader than strictly necessary — any file called&lt;br&gt;
&lt;code&gt;telemetry.env&lt;/code&gt; anywhere is now ignored — which for a secret is the correct&lt;br&gt;
direction to be wrong in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would take from this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A gitignore pattern with a slash in it is coupled to your directory&lt;br&gt;
structure.&lt;/strong&gt; For build output that is fine; if &lt;code&gt;dist/&lt;/code&gt; moves, you find out&lt;br&gt;
because your build breaks. For secrets it is a silent coupling to something you&lt;br&gt;
will eventually change for unrelated reasons, and the failure mode is that the&lt;br&gt;
secret becomes committable at the exact moment you are busy thinking about&lt;br&gt;
something else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer name-only patterns for anything sensitive.&lt;/strong&gt; &lt;code&gt;telemetry.env&lt;/code&gt; rather than&lt;br&gt;
&lt;code&gt;config/telemetry.env&lt;/code&gt;. &lt;code&gt;*.p12&lt;/code&gt; rather than &lt;code&gt;keys/*.p12&lt;/code&gt;. You lose a little&lt;br&gt;
precision and gain immunity to every future refactor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Look at &lt;code&gt;git status&lt;/code&gt; after a structural change, specifically for new untracked&lt;br&gt;
files.&lt;/strong&gt; Not for the files you moved — git shows those as renames and they are&lt;br&gt;
fine. For the ones that appear from nowhere. Anything newly untracked after a&lt;br&gt;
move was previously ignored by an anchored pattern, and that is the entire class&lt;br&gt;
of this bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And run &lt;code&gt;git check-ignore -v&lt;/code&gt; on your secrets after moving anything.&lt;/strong&gt; It takes&lt;br&gt;
one command and it is the only way to get a straight answer, because the&lt;br&gt;
dangerous result is the one that prints nothing at all.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://extendpilot.com" rel="noopener noreferrer"&gt;ExtendPilot&lt;/a&gt; shares a Mac's screen with the iPhones and iPads already in the&lt;br&gt;
room — as a mirror, or as a second desktop macOS treats as real hardware. Peer&lt;br&gt;
to peer over your own Wi-Fi, no account, no server in a session. It is not out&lt;br&gt;
yet. This one came out of restructuring the repo to hold an Android app&lt;br&gt;
alongside the Swift one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>git</category>
      <category>security</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>AWDL goes quiet every 528 ms, and it looks exactly like your bug</title>
      <dc:creator>Vishwas Prakash</dc:creator>
      <pubDate>Wed, 02 Sep 2026 09:52:12 +0000</pubDate>
      <link>https://dev.to/vishwas3000/awdl-goes-quiet-every-528-ms-and-it-looks-exactly-like-your-bug-2dja</link>
      <guid>https://dev.to/vishwas3000/awdl-goes-quiet-every-528-ms-and-it-looks-exactly-like-your-bug-2dja</guid>
      <description>&lt;p&gt;The pointer stuttered. Not badly, and not always, but enough that everyone who&lt;br&gt;
saw the app noticed it before they noticed anything else. We spent a long time&lt;br&gt;
looking for the cause in our code, and it was not there. It was in the radio,&lt;br&gt;
and the radio was doing exactly what it is designed to do.&lt;/p&gt;

&lt;p&gt;AWDL — Apple Wireless Direct Link — is the peer-to-peer Wi-Fi that AirDrop,&lt;br&gt;
AirPlay and MultipeerConnectivity all sit on. It is barely documented. If you&lt;br&gt;
are building anything realtime on MultipeerConnectivity you will meet its&lt;br&gt;
behaviour, and the default assumption when you do is that you have written a&lt;br&gt;
bug. This is what we measured, so that the next person can skip the week.&lt;/p&gt;
&lt;h2&gt;
  
  
  The link was idle when it stalled
&lt;/h2&gt;

&lt;p&gt;The first suspect was our own video. In ExtendPilot video is over 99% of the&lt;br&gt;
traffic and everything else is noise, so a pointer packet losing a race with a&lt;br&gt;
video burst was the obvious story.&lt;/p&gt;

&lt;p&gt;The numbers said no. Joining the host's records to the viewer's on a shared&lt;br&gt;
sequence number puts every stall on the host's timeline, and in the 300 ms&lt;br&gt;
before a typical stall the median amount of video sent was &lt;strong&gt;3.0 KB — about&lt;br&gt;
0.08 Mbit/s — against a run average of 1.58 Mbit/s.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Twenty times below the rate the link was comfortably carrying the rest of the&lt;br&gt;
time. Whatever stopped the pointer was not us competing with ourselves.&lt;/p&gt;
&lt;h2&gt;
  
  
  The stalls were periodic
&lt;/h2&gt;

&lt;p&gt;The intervals between them read 528 → 528 → 528 → 512 → 528 → 528. Across 97&lt;br&gt;
episodes the &lt;strong&gt;median interval was 528 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Networks do not usually fail on a metronome. Contention is bursty, interference&lt;br&gt;
is bursty, a neighbour's microwave is bursty. A number that repeats to within a&lt;br&gt;
few milliseconds is a schedule, and a schedule belongs to something that has a&lt;br&gt;
clock.&lt;/p&gt;

&lt;p&gt;AWDL synchronises its peers on a period of &lt;strong&gt;512 TU&lt;/strong&gt;. A TU is a time unit from&lt;br&gt;
the 802.11 spec — 1024 microseconds — so 512 TU is &lt;strong&gt;524.288 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvy4bjmhljnbz6iy4visl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvy4bjmhljnbz6iy4visl.png" alt="A timeline of AWDL availability windows 528 ms apart. A pointer update becomes ready just after one closes and waits the full period for the next." width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nothing is dropped and nothing is retried. The update is simply not sent until the radio comes back, which is why every loss metric we had reported a healthy link.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;528 against 524. That match is close enough that the conclusion is hard to&lt;br&gt;
avoid: these are the radio's own availability windows, and our packets were&lt;br&gt;
waiting for the next one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the radio has windows at all
&lt;/h2&gt;

&lt;p&gt;AWDL works by channel-hopping. The device has one antenna, and it time-slices&lt;br&gt;
between the infrastructure channel — the one your router is on — and the AWDL&lt;br&gt;
social channel where peers find each other. Peers agree a schedule so they are&lt;br&gt;
awake at the same moments, and between those moments the peer-to-peer link&lt;br&gt;
simply is not there.&lt;/p&gt;

&lt;p&gt;That is not a fault. It is how a single radio serves two networks at once. But&lt;br&gt;
it means peer-to-peer Wi-Fi has periodic latency spikes &lt;strong&gt;by design&lt;/strong&gt;, and any&lt;br&gt;
protocol you run over it inherits them.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three reasons it presents as your bug
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You did not choose the radio, and you are not told which one you got.&lt;/strong&gt;&lt;br&gt;
MultipeerConnectivity picks between infrastructure Wi-Fi, AWDL, and Ethernet on&lt;br&gt;
macOS. The framework decides; there is no API that reports the decision. The&lt;br&gt;
same code on the same two devices can behave differently on Tuesday because the&lt;br&gt;
link underneath it changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reliable delivery hides the stall and then hands you a burst.&lt;/strong&gt; A &lt;code&gt;.reliable&lt;/code&gt;&lt;br&gt;
send is ordered, so a radio that goes away for 300 ms does not drop your&lt;br&gt;
frames — it queues them, and delivers the whole backlog the moment the window&lt;br&gt;
opens. On glass that is freeze, then fast-forward, then normal. Delivery ratio&lt;br&gt;
stays around 99% throughout. Every loss-shaped metric reads healthy while the&lt;br&gt;
picture is visibly wrong, which is why loss was never the explanation and why&lt;br&gt;
we kept looking somewhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The delay distribution is bimodal, and averages erase it.&lt;/strong&gt; Half our packets&lt;br&gt;
arrived within 4 ms of the best transit ever seen. Fifteen per cent arrived&lt;br&gt;
more than 120 ms behind it. There is very little in between. A packet either&lt;br&gt;
goes straight through or it waits for the next window — and a mean transit time&lt;br&gt;
computed over both populations describes neither of them, and looks fine.&lt;/p&gt;
&lt;h2&gt;
  
  
  You cannot prevent it, only absorb it
&lt;/h2&gt;

&lt;p&gt;Nothing in an app can stop a radio going quiet on its own schedule. There is no&lt;br&gt;
flag, no priority class, no way to ask for the window. The only thing available&lt;br&gt;
is to stop letting the radio's timing become the display's timing.&lt;/p&gt;

&lt;p&gt;That means a jitter buffer: hold arriving data briefly, then play it out on the&lt;br&gt;
sender's clock rather than on arrival. Jitter smaller than the hold becomes&lt;br&gt;
invisible. Ours is adaptive, 50–160 ms, and it took frozen pointer frames from&lt;br&gt;
&lt;strong&gt;12.8% to 2.7%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is worth being clear about what that fix does not do. The stalls are still&lt;br&gt;
there, exactly as often. We are paying up to 160 ms of latency to make them&lt;br&gt;
stop being visible, which is a trade rather than a repair.&lt;/p&gt;

&lt;p&gt;One more consequence, if you were thinking of routing around it: moving video&lt;br&gt;
onto its own Network.framework lane would not help, because both lanes ride the&lt;br&gt;
same radio.&lt;/p&gt;
&lt;h2&gt;
  
  
  What to do about this in your own app
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Find out which radio you are actually on.&lt;/strong&gt; This takes one command on the Mac,&lt;br&gt;
during a live session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netstat &lt;span class="nt"&gt;-I&lt;/span&gt; awdl0 &lt;span class="nt"&gt;-w&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Non-zero traffic on &lt;code&gt;awdl0&lt;/code&gt; means peer-to-peer Wi-Fi is carrying your session.&lt;br&gt;
Zero means it is not, and your periodic stalls are something else. This is the&lt;br&gt;
measurement to take first, before any tuning, because it decides which half of&lt;br&gt;
the problem space you are in. &lt;code&gt;nettop -m route -t wifi -J bytes_in,bytes_out&lt;/code&gt;&lt;br&gt;
gives you the same answer from the other direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run the same session twice, with the phone's Wi-Fi off and on.&lt;/strong&gt; Wi-Fi off&lt;br&gt;
forces pure AWDL. Wi-Fi on and joined to the same SSID as the Mac allows&lt;br&gt;
infrastructure. If the stutter has a different texture between the two runs,&lt;br&gt;
the radio is in your causal chain. This costs two minutes and it is the closest&lt;br&gt;
thing to an A/B test available on a link you cannot select.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not let your recovery path fire on the radio's period.&lt;/strong&gt; If a stall&lt;br&gt;
triggers a retry, and the stall is periodic, so are the retries — and a retry&lt;br&gt;
sent to a peer that is not currently listening is pure added load. We caused&lt;br&gt;
exactly this once by dropping a recovery interval to a flat 150 ms and produced&lt;br&gt;
360 requests in 107 seconds, each one making the next hole more likely. Back&lt;br&gt;
off exponentially, and treat any recovery message that must be sent reliably as&lt;br&gt;
expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A fixed jitter buffer is the wrong shape for a bimodal input.&lt;/strong&gt; This is the&lt;br&gt;
part that generalises furthest. Two regimes that want opposite things cannot be&lt;br&gt;
served by one constant. Sized for the common case — 80 ms, comfortably over the&lt;br&gt;
4 ms that half our packets needed — the buffer ran dry on every single stall.&lt;br&gt;
Sized for the tail at 250 ms, it worked, and charged every user a quarter of a&lt;br&gt;
second of lag permanently to handle a case that occurs 15% of the time.&lt;/p&gt;

&lt;p&gt;Make it adaptive, and make it asymmetric: rise fast, decay slow. Reacting late&lt;br&gt;
to a worsening link costs a visible freeze; lingering on a recovering one costs&lt;br&gt;
a little lag nobody notices. It is the same shape as TCP's AIMD, for the same&lt;br&gt;
reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check whether your metric can see the failure at all.&lt;/strong&gt; Ours could not.&lt;br&gt;
Delivery ratio said 99%, the send backlog said healthy, and the picture stalled&lt;br&gt;
anyway. If your instrumentation counts what arrives, it cannot describe silence.&lt;/p&gt;

&lt;p&gt;Caveats on all of the above: one device pair, one link, 60-second runs. The&lt;br&gt;
528 ms figure is ours, on our hardware. The 512 TU period is AWDL's, and yours&lt;br&gt;
will be the same.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://extendpilot.com" rel="noopener noreferrer"&gt;ExtendPilot&lt;/a&gt; shares a Mac's screen with the iPhones and iPads already in the room — as a mirror, or as a second desktop macOS treats as real hardware. Peer to peer over your own Wi-Fi, no account, no server in a session, so it works with the router unplugged. Built for the meeting room whose HDMI cable never works, and for the iPad on the desk doing nothing. The Mac app is a free notarised download, or &lt;code&gt;brew install vishwas3000/tap/extendpilot&lt;/code&gt;; the iPhone and iPad app is with App Review. Every performance claim about it traces to a session like the one above, written down with the number that was true before and the number that was true after. This one is a number we could not change, only work around.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>debugging</category>
      <category>performance</category>
      <category>ios</category>
    </item>
    <item>
      <title>You don't need a remote desktop for the room you're standing in</title>
      <dc:creator>Vishwas Prakash</dc:creator>
      <pubDate>Wed, 02 Sep 2026 09:51:44 +0000</pubDate>
      <link>https://dev.to/vishwas3000/you-dont-need-a-remote-desktop-for-the-room-youre-standing-in-2k5</link>
      <guid>https://dev.to/vishwas3000/you-dont-need-a-remote-desktop-for-the-room-youre-standing-in-2k5</guid>
      <description>&lt;p&gt;Someone plugs in the HDMI cable. The room's display shows nothing, or shows 1024×768, or shows the desktop of whoever presented last week. After a minute of this, somebody says "just share your screen", and out comes AnyDesk or TeamViewer.&lt;/p&gt;

&lt;p&gt;It works. It is also the wrong shape for the problem, and noticing why turns out to be more interesting than it sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What remote desktop tools are actually for
&lt;/h2&gt;

&lt;p&gt;AnyDesk, TeamViewer and RustDesk exist to solve one problem well: &lt;strong&gt;reach a machine you are not near.&lt;/strong&gt; Your parent's laptop. A server in a rack. A colleague's desktop three time zones away. Everything about their design follows from that.&lt;/p&gt;

&lt;p&gt;One person connects to one machine. That person takes the mouse. The remote screen is mirrored to them, and the whole session is framed as &lt;em&gt;control&lt;/em&gt; — because when you are not in the room, control is the only way to do anything.&lt;/p&gt;

&lt;p&gt;Being fair about the privacy question, because it is the claim people reach for first and it is wrong: &lt;strong&gt;AnyDesk has a LAN mode&lt;/strong&gt;, and in it a session goes directly between the two machines without the internet. RustDesk can be &lt;a href="https://rustdesk.com/" rel="noopener noreferrer"&gt;self-hosted&lt;/a&gt; entirely on your own infrastructure. Neither of these tools forces your screen through somebody's cloud if you configure them not to. If you have read that they do, that is not accurate.&lt;/p&gt;

&lt;p&gt;The mismatch is not privacy. It is shape.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo70956bb6trss3jd7bqi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo70956bb6trss3jd7bqi.png" alt="Two network topologies. A remote desktop routes viewer to relay server to host across the internet; local sharing connects viewer to host directly on the same Wi-Fi." width="799" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The relay, the account and the NAT traversal are not overhead — they are the product, and they are what makes reaching a machine in another country possible at all. Standing beside the machine, you pay for all three and use none of them.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the shape stops fitting
&lt;/h2&gt;

&lt;p&gt;Stand in a room with four people and a Mac, and three things go wrong at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is one-to-one.&lt;/strong&gt; Remote desktop is a session between two endpoints. Four people looking at one screen is not what it models, so three of them read over a shoulder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It mirrors, and mirroring is sometimes the wrong answer.&lt;/strong&gt; Every tool in this category shows a screen that already exists. But the meeting-room problem is often the opposite: you want a screen you &lt;em&gt;do not have&lt;/em&gt; — somewhere to put the thing you are talking about while your laptop keeps the notes. No remote desktop tool creates a display, because from three time zones away that idea makes no sense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control is the default, when nobody wanted control.&lt;/strong&gt; Handing over the mouse is the whole premise. In a room, everyone wants to point at something and one person occasionally wants to click.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else is out there
&lt;/h2&gt;

&lt;p&gt;Honestly, and including the ones that do the job better than we do.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Account&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sidecar&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apple's own. iPad as a real second display, mirror or extend&lt;/td&gt;
&lt;td&gt;Same Apple Account on both&lt;/td&gt;
&lt;td&gt;Free, built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Apple Screen Sharing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mac to Mac over the LAN, built into macOS&lt;/td&gt;
&lt;td&gt;Local account on the target&lt;/td&gt;
&lt;td&gt;Free, built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/pavlobu/deskreen" rel="noopener noreferrer"&gt;Deskreen&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open source. Streams a screen or one window to any browser on the LAN&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Free, AGPL-3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AnyDesk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Remote desktop, LAN mode available&lt;/td&gt;
&lt;td&gt;Yes for most use&lt;/td&gt;
&lt;td&gt;Free tier capped at 3 devices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TeamViewer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Remote desktop, LAN mode available&lt;/td&gt;
&lt;td&gt;Yes for most use&lt;/td&gt;
&lt;td&gt;Free personal; commercial use cuts sessions at 5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RustDesk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open source remote desktop, self-hostable&lt;/td&gt;
&lt;td&gt;Optional if self-hosted&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Duet Display&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;iPad as a second display, wired or wireless&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Luna Display&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;iPad as a second display, via a hardware dongle&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Paid + hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If you have an iPad and a Mac and both are on your Apple Account, use Sidecar.&lt;/strong&gt; It is free, it is built in, it extends rather than mirrors, and it is genuinely good. Anyone telling you otherwise is selling something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And if you want no account at all, look at Deskreen before you look at us.&lt;/strong&gt; It is the one on that list closest to the same argument — free, open source, local network, nothing to sign in to, and the viewing device needs no app because the viewer is a browser tab. It is 21,000 stars and still shipping; v3.2.16 went out in July 2026. For getting a screen onto the other devices in a room it is an excellent answer and it costs nothing to try.&lt;/p&gt;

&lt;p&gt;Where it differs is the same line the rest of this post is about. Deskreen streams a screen or a single window into a browser, so what the iPad shows is a picture of something the Mac was already displaying. It cannot give macOS a display that is not there — a browser tab is not a monitor the window server can place windows on — which is the thing that decides whether you are mirroring a screen or gaining one.&lt;/p&gt;

&lt;p&gt;That distinction is worth being precise about, because "second screen" gets used for both and they are not the same feature.&lt;/p&gt;

&lt;p&gt;Sidecar stops being the answer when the second screen belongs to someone else — a colleague's iPad, a client's iPhone, a device that is not on your Apple Account. That is a different problem, and it is the one we were trying to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where ours sits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://extendpilot.com" rel="noopener noreferrer"&gt;ExtendPilot&lt;/a&gt; is built for the room rather than the continent, and the design falls out of that in three ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Several people watch at once.&lt;/strong&gt; Everyone in the room joins the same screen. They can point at it, draw on it, and react, all at the same time, each in their own colour. Only one person can be handed the pointer, and that is a deliberate grant the presenter makes and takes back by touching their own mouse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can extend, not only mirror.&lt;/strong&gt; It creates a display that macOS treats as real hardware — arrangeable in System Settings, HiDPI, windows move onto it. That is the part remote desktop tools structurally cannot do, and &lt;a href="https://extendpilot.com/blog/the-display-macos-wont-ship" rel="noopener noreferrer"&gt;it is also why it cannot be in the Mac App Store&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No account, and no server in a session.&lt;/strong&gt; Devices find each other with Bonjour and connect peer to peer. There is nothing to sign in to, and a session works with the router unplugged. Not as a policy — there is simply no infrastructure in the path to send it through.&lt;/p&gt;

&lt;p&gt;It is free. The Mac app is a notarised download from &lt;a href="https://extendpilot.com" rel="noopener noreferrer"&gt;extendpilot.com&lt;/a&gt;, or &lt;code&gt;brew install vishwas3000/tap/extendpilot&lt;/code&gt;; the iPhone and iPad app is with App Review.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use it
&lt;/h2&gt;

&lt;p&gt;Worth saying, because a comparison that recommends its author's software in every case is not a comparison.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You are not in the room.&lt;/strong&gt; Use AnyDesk, RustDesk or Screens. Ours needs both devices on the same Wi-Fi and offers nothing over the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need unattended access to a machine nobody is sitting at.&lt;/strong&gt; Not a thing it does. MeshCentral or RustDesk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to share from Windows or Linux.&lt;/strong&gt; Sharing is macOS only. Watching will come to Android; sharing will not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both devices are yours and on one Apple Account.&lt;/strong&gt; Sidecar is right there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The useful question is not which tool is best. It is whether the thing you are doing is remote at all — and in a meeting room, it usually is not.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>macos</category>
    </item>
    <item>
      <title>The Mac got slower, and quitting the app didn't fix it</title>
      <dc:creator>Vishwas Prakash</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:48:35 +0000</pubDate>
      <link>https://dev.to/vishwas3000/the-mac-got-slower-and-quitting-the-app-didnt-fix-it-1jod</link>
      <guid>https://dev.to/vishwas3000/the-mac-got-slower-and-quitting-the-app-didnt-fix-it-1jod</guid>
      <description>&lt;p&gt;This is the worst bug I have written. Not because the app broke — it worked fine throughout — but because it degraded the machine it ran on, permanently, and left the damage behind after quitting. Someone could have uninstalled the app and still been slow, with nothing to connect the two.&lt;/p&gt;

&lt;p&gt;ExtendPilot puts a real second desktop on an iPad: not a mirror, but a display macOS treats as hardware, arrangeable in System Settings, with windows that move onto it. There is no public API for that, so it sits on a private one. This is the story of what the private one does when you hold it wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looked like
&lt;/h2&gt;

&lt;p&gt;After a day of development the Mac had a load average around 6 and felt unusable. Three ColorSync daemons were burning about 115% CPU between them — over a full core — and had been for 47 minutes at the point of measurement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;top &lt;span class="nt"&gt;-l&lt;/span&gt; 2 &lt;span class="nt"&gt;-o&lt;/span&gt; cpu
&lt;span class="go"&gt;colorsyncd                 46.5%
colorsync.displayservices  35.6%
colorsync.useragent        33.2%
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They were not logging anything. A full &lt;code&gt;log show&lt;/code&gt; came back empty, so nothing pointed at them at all. Two of the three run as root; sampling the one running as the user gave the hot stack immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sample 96211 3
&lt;span class="go"&gt;  21  ColorSyncXPCDeviceRegistryCopyAnyUserInfo
  20  ColorSyncDeviceRegistryCopyInfo
  11  ColorSyncIterateDeviceProfiles
  10  ColorSyncProfileCreateWithURLAndOptions
   7  ColorSyncCreateCFDataFromURL
  40  __CFBinaryPlistCreateObjectFiltered
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Iterating the display device registry and re-reading profiles off disk, in a loop. So the question became how big the registry had got.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; /Library/ColorSync/Profiles/Displays/ | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;97
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; /Library/ColorSync/Profiles/Displays/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; LocalScreenShare
&lt;span class="go"&gt;72
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;find /Library/ColorSync/Profiles/Displays/ &lt;span class="nt"&gt;-mtime&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="go"&gt;77
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seventy-two of ninety-seven profiles were ours, and seventy-seven of them had been created that day. Every one was an ICC profile that &lt;code&gt;colorsyncd&lt;/code&gt; had written for a monitor it had never seen before. We had been handing it a brand new monitor every few minutes, and it had been dutifully profiling each one and keeping the result forever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9ay163779dcdtiw7uim.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9ay163779dcdtiw7uim.png" alt="A step chart of ColorSync profiles written during one session, climbing to 72. The line does not fall at the marker where the app was quit."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The line not falling at the quit marker is the part that made this hard to find. Every instinct says a leak ends when the process does, so the machine stayed slow long after the obvious suspect had exited.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause is one line
&lt;/h2&gt;

&lt;p&gt;A virtual display is created from a descriptor. Three of its fields build the display's identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;nextIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;productID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x4C53&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vendorID&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x3456&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialNum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;getpid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextIndex&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;vendorID&lt;/code&gt; / &lt;code&gt;productID&lt;/code&gt; / &lt;code&gt;serialNum&lt;/code&gt; triple &lt;em&gt;is&lt;/em&gt; a display's identity to macOS. Mixing in &lt;code&gt;getpid()&lt;/code&gt; meant that identity changed on every launch. macOS concluded, correctly, that it had never seen this monitor before, and &lt;code&gt;colorsyncd&lt;/code&gt; did exactly what it is supposed to do for a new monitor: profile it, and keep the profile. Around 36 launches, two desktops each, is 72 profiles.&lt;/p&gt;

&lt;p&gt;The bitter part is that this line was itself a fix. An earlier version gave every display the same serial number, and macOS then treated two simultaneous desktops as one monitor — each inheriting the other's saved arrangement and coming up at the wrong size. Adding the pid made them distinct, and it worked. It also made them distinct &lt;em&gt;across time&lt;/em&gt;, which nobody had asked for and nobody noticed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An identity that had to be &lt;strong&gt;stable&lt;/strong&gt; was derived from something &lt;strong&gt;ephemeral&lt;/strong&gt;. The requirement was "distinct from its siblings". The implementation delivered "distinct from everything, forever".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The first fix was not enough
&lt;/h2&gt;

&lt;p&gt;Replacing the pid with a hash of the display's own size and slot fixes the across-launch case, because both of those are properties of the desktop rather than of the process that made it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialNum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x1FFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x1FFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;slot&lt;/code&gt; still came from &lt;code&gt;nextIndex++&lt;/code&gt;, a counter that only ever goes up. Within a single session, removing a desktop and adding another advanced it — and applying a saved preset does exactly that, removing every desktop and recreating them. Applying a preset three times minted six identities without the app ever restarting.&lt;/p&gt;

&lt;p&gt;The file timestamps said so plainly: profiles written at 00:45, 00:58, 01:04, 01:06 and 01:13. Minutes apart, inside one session. The fix is that the slot is now passed in by the caller, which already knows each desktop's position in its own list — it does not need a counter to tell it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A counter that only increments is the subtle version of this bug. It looks stable because it is deterministic. A counter is a clock in disguise.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Neither was the second
&lt;/h2&gt;

&lt;p&gt;Creating a display can fail if another process already holds that identity, so there is a fallback path. In the same commit that fixed the two problems above, the fallback read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialNum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;getpid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ← pid again&lt;/span&gt;
    &lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same mistake, a third time, written by someone who had spent the previous hour understanding why it was a mistake. It fires rarely, but when it fires it is unbounded. It is now four bounded alternates, so a given size costs at most four profiles ever, rather than one per launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight objective_c"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialNum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x1FFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x1FFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                         &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x0F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;displayClass&lt;/span&gt; &lt;span class="nf"&gt;alloc&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="nf"&gt;initWithDescriptor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three passes to get one line right. Worth remembering the next time a fix feels obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  A postmortem without a check is just a story
&lt;/h2&gt;

&lt;p&gt;The regression check creates the two desktops, releases them, counts the profiles on disk, creates the same two again, releases them, and counts again. If a repeat run ever mints a new identity, the number moves and the check fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;display profiles on disk: 27 before the repeat, 27 after
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing that check turned up a second bug worth recording, because it is a good example of a test being right for the wrong reason. The first version passed the displays back through out-parameters. Under ARC those are &lt;code&gt;__autoreleasing&lt;/code&gt;, so the pool held the displays alive well past the point where the test released them, and the check failed with &lt;em&gt;"a virtual display outlived its owner"&lt;/em&gt;. The check was correct. The check's own helper was the thing leaking. Each round now gets its own &lt;code&gt;@autoreleasepool&lt;/code&gt;, and there is a comment next to it explaining why no helper exists.&lt;/p&gt;

&lt;p&gt;The code fix stops new profiles. It cannot remove the seventy-two already written, which are root-owned and have to be deleted by hand. That asymmetry is the part I would most like other people to take away from this: the damage outlived the process, the fix does not reach backwards, and no amount of care after the fact recovers it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for in your own code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Identity derived from something ephemeral.&lt;/strong&gt; Anything the OS persists or registers under a key you generate must have that key derived &lt;em&gt;only&lt;/em&gt; from what genuinely distinguishes the thing. Audit any use of &lt;code&gt;getpid()&lt;/code&gt;, a timestamp, a UUID, a random number or a monotonic counter that feeds a serial number, a filename, a bundle id, a service name, a Keychain account, a Bonjour name or a cache key.&lt;/p&gt;

&lt;p&gt;The question that finds it: &lt;em&gt;if this runs a thousand times, how many distinct values does the OS end up storing?&lt;/em&gt; If the answer is a thousand, this is your bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources the OS keeps after you exit.&lt;/strong&gt; Virtual displays go away when released; the profiles do not. Before using an API that registers something system-wide, ask what survives the process, and whether anything ever cleans it up. In our case nothing did, and still does not — the fix is that we stop creating them, not that we tidy up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silent hot loops.&lt;/strong&gt; &lt;code&gt;colorsyncd&lt;/code&gt; logged nothing at all while spinning. The only thing that found it was sampling the hot stack. When something is eating CPU and the logs are quiet, sample it rather than reasoning about it. And note that &lt;code&gt;ps&lt;/code&gt; reports &lt;code&gt;%CPU&lt;/code&gt; as a &lt;em&gt;lifetime average&lt;/em&gt; rather than a current reading — a 47-minute-old process showing 73% in &lt;code&gt;top -l 2&lt;/code&gt; has been there the whole time, which is a far stronger signal than a spike.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://extendpilot.com" rel="noopener noreferrer"&gt;ExtendPilot&lt;/a&gt; shares a Mac's screen with the iPhones and iPads already in the room — as a mirror, or as a second desktop macOS treats as real hardware. Peer to peer over your own Wi-Fi, no account, no server in a session, so it works with the router unplugged. Built for the meeting room whose HDMI cable never works, and for the iPad on the desk doing nothing. The Mac app is a free notarised download, or &lt;code&gt;brew install vishwas3000/tap/extendpilot&lt;/code&gt;; the iPhone and iPad app is with App Review. Every performance claim about it traces to a session like the one above, written down with the number that was true before and the number that was true after. So do the mistakes; this is one of them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>performance</category>
      <category>macos</category>
      <category>swift</category>
    </item>
  </channel>
</rss>
