Skip to content

About

Memoizing your list items cannot fix a thread that is already gone. This repo reproduces a synchronous deserialization lock in your browser, lets you switch on the frontend optimizations everyone reaches for first, and shows the freeze surviving all of them. Companion code for a writeup at zetesis.dev.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

jsi-thread-lock

Opening a project froze a construction site app for more than ten seconds. Not slow. Frozen. Taps did nothing, typing did nothing, and then everything the user had pressed happened at once.

The three fixes people reach for first do not help. This repo lets you prove that yourself. Load the project and watch a counter stop dead next to a spinner that keeps turning. Then switch on all three fixes and load it again. The freeze does not move.

Which side of the handover each fix works on

Run it

Three commands. You need Python 3.12 or newer and nothing else. No accounts, no keys, no node, no npm, no build step.

git clone https://github.com/Zetesis-dev/jsi-thread-lock
cd jsi-thread-lock
python run.py

That sets up a local environment, generates the demo data into web/generated/, starts the page and opens it at http://127.0.0.1:8002. Press Ctrl+C to stop it. On macOS and Linux, type python3 instead of python. Add --rebuild to start over from scratch.

Pick how the project loads, press the button, and press the number buttons while it runs.

The rest of it

Everything lands in a .venv folder. Activate it to reach the terminal version and the tests:

.venv\Scripts\activate           # Windows
source .venv/bin/activate        # macOS and Linux

python demo/before.py    # one long stretch of work   ->  THREAD HELD
python demo/after.py     # the same work, in slices   ->  THREAD FREE
pytest                   # the proof
python web/serve.py      # the page again, skipping the setup step

demo/before.py and demo/after.py are the same file apart from one line:

$ diff demo/before.py demo/after.py
11c11
< STRATEGY = strategies.parse_blocking
---
> STRATEGY = strategies.parse_chunked

Same data, same work, same target. The only thing that changed is whether the work ever stops to let anything else happen.

What you will see

Measured in Chromium on the laptop this was built on, at the middle data size of 500 inspections and 24,000 answers:

How the project loads Longest freeze Work done Redraws missed
All at once 1.1 s, in one stretch 1.1 s ~65
All at once, with all three fixes on 1.1 s, in one stretch 1.1 s ~65
In small pieces ~10 ms at worst, across ~126 pieces 1.1 s 0
Handed to a helper never froze 1.1 s 0

The second row is the point of the whole repo. All three fixes are switched on, all three are really implemented, and all three demonstrably do their job. The row cache reports 250 reused rows on a second load and the drawing time falls. The number that matters does not change at all.

The last row is the other half of it. The work did not get any quicker. It moved to a helper running alongside, where it no longer keeps anyone waiting, and only short labels came back.

Presses are timed by the browser at the moment your finger goes down, not when the app gets round to noticing. In one scripted run a press landing 413 ms into the freeze waited 711 ms to be handled. The three presses after the freeze ended waited between 1 and 3 ms.

Why none of the three fixes help

All three change how the screen is drawn. None of them change the unpacking that happens first. By the time any of them could run, the app has already spent the whole second and the user has already given up. The diagram at the top shows this as a shaded box: every suggested fix lives inside it, and the freeze happens to the left of it.

This is not an argument that the fixes are bad. Reusing rows, waiting for animations and keeping data in one place are all worth doing. They are answers to a different question.

What the tests prove

tests/test_thread_is_held.py shows the blocking version does its work in one unbroken stretch, with no pause anywhere inside it, lasting well past the point a person would notice. Not lots of small delays. One long one.

tests/test_frame_budget_is_never_exceeded.py shows that no single slice of the chunked version runs longer than 16.7 ms, the limit for motion that still looks smooth. That is what the fix actually buys. Not a quicker parse, a parse that always stops in time. Slices are cut by the clock rather than by a fixed count, so the guarantee still holds on a slow machine.

tests/test_frontend_fixes_do_not_help.py is the important one. It runs the three suggested fixes against the blocking path and checks that the longest freeze is unchanged. It also checks each fix is doing something real: the row cache is inspected for entries, the shared store is inspected for a write, and the deferral is timed to confirm the delay happened. A fix that did nothing would prove nothing.

What is real and what is a stand-in

This runs in a browser, not in React Native. The browser has the same shape of problem: one lane doing all the work, and one way out of it. A Web Worker stands in for a background thread on a phone. No part of this talks to a real device.

The data is kept small on purpose, 1.55 MB in the middle size and 4.65 MB at the largest. Handing a visitor's browser tens of megabytes to chew through is a memory risk, not a demonstration. The freeze comes from the effort spent on each answer instead, set by WORK_SCALE in web/modes/jsi.js, because a laptop is much quicker than the mid range site hardware where the original bug turned up. That number sets how long the freeze lasts. It cannot change the shape of it, which is one stretch of work with no break inside, and the shape is the only thing being argued about here. Lower it and every way of loading gets quicker together, while the gap between them stays exactly where it is.

The freeze itself is real. The counter is driven by the browser's own redraw callback with no code anywhere that fakes a pause, and the timeline can only draw the red bar once the work is over, because the page cannot redraw itself while it is frozen either.

Everything here is made up

This reconstructs a pattern from a real piece of work. The project, the drawings, the sheets, the inspectors, the areas and every checklist answer were invented for the reconstruction. None of it is the original code, and nothing here identifies the client or the system it came from.

Writeup

The engineering writeup this goes with is at zetesis.dev.

License

MIT. See LICENSE.

About

Memoizing your list items cannot fix a thread that is already gone. This repo reproduces a synchronous deserialization lock in your browser, lets you switch on the frontend optimizations everyone reaches for first, and shows the freeze surviving all of them. Companion code for a writeup at zetesis.dev.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages