Skip to content

Look up windows less often when the screen changes - #164

Open
zywek123 wants to merge 7 commits into
trypsynth:masterfrom
zywek123:window-changes
Open

zywek123 wants to merge 7 commits into
trypsynth:masterfrom
zywek123:window-changes

Conversation

@zywek123

@zywek123 zywek123 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

When the screen changes, the window interpreter asked Android for the list of windows 13 to 16 times for each window event, and again at each 10 ms check while it waited for the windows to settle, for up to 550 ms, or 200 ms with animations off. Most of those lookups were for roles that have no window, such as the second app of a split screen, so they never succeeded. In the new test, a screen change that waits the full 550 ms asked for the window list 913 times, and 275 times with the shorter wait. A window without a title also had its root fetched from its app at every check, to be named after the app, and that waits for the app's main thread, which is busy drawing the new screen.

The first commit adds tests that record everything the interpreter reports for common screen changes. The others change how windows are looked up, each leaving those tests as they are:

  • No window lookups for roles that have no window
  • One window list for each check, still fetched again at every check, so that windows that change without an event are noticed as before
  • The package of each window's root remembered by window id, so that a window without a title is fetched once. The focus history names the window on every focus change, so this also helps there.
  • Direct touch checks its switches, and whether it has seen the app before, before it fetches window roots and asks the package manager on every window change
  • restoreLastFocusedNode fetches the window root only when there is a focus to restore, and only once (Wear OS 6 (Galaxy Watch 5 Pro): first utterance after a service restart 0.06-1.2 s late, and display-state work blocks the main thread up to 1.6 s #152)

Measured on a Galaxy S10 (LineageOS 22.1, Android 15) and an API 36 emulator, going round six Settings pages (Internet, Accessibility, Sound, Display, Apps, Date and time), each opened from Settings and closed with back, in an order that changes every round: 24 openings and 24 backs on each build. Counters in both builds count the window lists Backtalk asks for and time its main thread work, and on the emulator Android's accessibility trace records Backtalk's calls into the system. Medians:

Per screen change, opening / back master this PR
Window lists fetched (S10) 96 / 103 7 / 7
Window roots fetched from apps (emulator) 10 / 14 8 / 10
Binder calls from Backtalk (emulator) 111 / 68 93 / 55
Direct touch, main thread time (S10) 34 / 34 ms 5 / 6 ms
Restoring the last focus (S10) 6.7 / 11.8 ms 0.2 / 8.9 ms
Window interpreter with its listeners (S10) 61 / 79 ms 58 / 73 ms
Time to the first speech (S10) 492 / 261 ms 490 / 268 ms

The time to the first speech is the same, since it depends on when the windows settle, which this PR doesn't change. Android's cache serves most window lists, so each costs little and the interpreter takes about as long as before. The main thread time saved is direct touch's and the focus restore's, about 30 ms per screen change.

Not tried on a watch yet. The watch from #152 can try it in the development build once this is merged.

WindowEventInterpreterTest feeds the window interpreter the window events of common screen changes, with the windows Android would list at each moment, and records everything a listener can read from each interpretation, and when it came. The screen changes are a new screen named by its state change, one without such a name, the shorter wait when animations are off, a window that closes before the system reports it, split screen, the keyboard, picture-in-picture, a window without a title, a pane, the screen turning off and on, and the volume panel. The tests pin down what the interpreter reports today, so that changes to how it looks up windows can show that the rest of Backtalk is told the same.

The main looper is paused, so each 10 ms check runs at a known time, and the service counts how often it is asked for the window list. A screen change that waits the full 550 ms asks for it 913 times.
At a window event, and at each 10 ms check while it waits for the windows to settle, the window interpreter looks up the title of the window in each of its six roles, and the app of each window without a title. A role without a window, such as the second app when the screen isn't split, has the id -1, which no window has. Looking up its title missed the kept windows, so the interpreter fetched them again, and looking up its app fetched them once more. On a screen with one app, and no split screen, dialog, picture-in-picture window or keyboard, each check asked Android for the windows 13 times. These lookups now return at once, with the same result, so such a check asks once, to map the windows to roles.

The failed lookups also refreshed the kept windows at each check. The titles now come from the windows kept since the last window event, which Android sends for every change to the windows, and a window missing from them still makes the interpreter fetch them again. A later commit gives each check a list of its own.

In the tests, a screen change that waits the full 550 ms asks for the window list 229 times instead of 913, and one that waits 200 ms with animations off asks 23 times instead of 275. What the interpreter reports is unchanged.
At a window event, the window interpreter asked Android for the windows up to three times: to note their titles, to map them to roles, and for the titles of the windows in those roles. Each check while it waited for the windows to settle asked again to map them to roles. The window an event names is often gone, as when the event reports that it closed. It was missing from the kept windows at each lookup, so each check fetched them twice more for its title, and once more for its app, since looking up a window's app always fetched the windows anew.

Now each window event and each check fetches the windows once, and all its lookups use that list: titles, roles and apps. A window missing from it isn't looked for in a list fetched a moment later. Each check still fetches its own list, so windows that change before their windows event are noticed as before, and the titles now come from the same list as the roles. Lookups from the rest of Backtalk use the list of the last event or check, and fetch the windows again for a window missing from it, as title lookups did. Looking up a window's app now works that way too. The focus history names the window at every focus change, and in a window without a title that looked up its app, fetching the windows each time.

In the tests, each event and each check asks for the window list once. A screen change that waits the full 550 ms asks 57 times, against 229 after the previous commit and 913 before it, and one that waits 200 ms asks 21 times. What the interpreter reports is unchanged.
A window without a title is named after its app, which the window interpreter found by fetching the window's root and reading its package. Fetching a root waits for the app's main thread, which is busy drawing while the screen changes, and the interpreter fetched it at every check while it waited for the windows to settle, and again whenever the rest of Backtalk asked for the window's title. The focus history asks at every focus change.

The package of each window's root is now kept by window id, since a window keeps its id until it closes and no other window gets it. It is used only while the window is listed, so a window that has closed is untitled as before, and the packages of windows that are gone are dropped at each windows event. A root that can't be fetched, as before its app has drawn it, isn't kept, so it is asked for again at the next lookup.

In the tests, a window without a title has its root fetched once while the screen changes to it and back, instead of 59 times. A window whose root can't be fetched at first is named after its app at the first check after it can be, with one fetch instead of one at every check. What the interpreter reports is unchanged.
…e manager

Direct touch looks at the screen 150 ms after every window change and every window interpretation, even when it isn't turned on for any app. Each look fetched the root of the app window in front, of every other application window and of every system window, which waits for each window's app, and read the app's manifest with up to three package manager queries, before it checked a single switch. The main thread stalls on a watch in trypsynth#152 included this look, run then by screen changes, which 299eab3 dealt with. Window changes still ran it in full.

Now each look asks only for what it needs:
- The package of a window's root comes from the window interpreter, which keeps it by window id, so the app in front is asked once per window.
- The manifest is read only the first time an app is seen, the only time it is used.
- The checks that need other windows' roots run only when the switches, the window types and the focus leave the answer open. Another app's window can only be found when an application window other than the game is open, and that already turns direct touch off as a dialog, so those roots aren't fetched at all now. A system window is asked whether it belongs to System UI only when it is active and covers half the screen, as the notification shade does.

What direct touch decides, says and sends as the passthrough region is unchanged.

A new test feeds window changes through the window interpreter to direct touch, as the service does. Three window changes over a game that isn't turned on fetched 23 window roots and read the manifest 7 times, and now fetch one root and read the manifest once. With the game turned on, and the notification shade opening and closing over it, 30 roots and 9 manifest reads became 2 and 1. DirectTouchPolicyTest goes through all 128 combinations of inputs: the answer is the same as before, and the checks that need roots run only when they can change it.
When the screen changes, Backtalk first tries to restore the focus the user last had in the window that is now active. restoreLastFocusedNode fetched that window's root before it looked in the focus history, so a new screen, which has no focus to restore, had its root fetched for nothing, and a later step that focuses the first item fetches it again. The fetch waits for the window's app, which is busy drawing the new screen, and on a watch in trypsynth#152 it was among the main thread stalls after the screen turned on.

The root is now fetched only once the history has a focus for the window, and after the check that keeps a focus from an app out of a System UI window. Restoring a focus also fetched the root a second time, to check that the node is still in the window. That check now uses the root fetched already.

What gets focused is unchanged. In a new test, a screen without a focus to restore fetches no root instead of one, and so does a System UI window whose last focus came from an app, while a focus that is restored fetches the root once instead of twice.
…ents

FocusActorForScreenStateChangeTest checked that a focus from an app isn't restored into a System UI window by seeing the restore fail without fetching the root. The node it recorded wasn't in the shade's tree, though, so without that
rule the restore would have failed anyway, only later. Both windows now share one root that holds the node, and the test checks that the history has a focus for the shade's window id, so only the rule stops the restore. With the rule
taken out, the test fails. An unused import is gone.

The comment on fetching the root in restoreLastFocusedNode used "only once" in two senses, and the comment on direct touch's window interpreter read oddly. Both now say plainly what they mean.
@zywek123
zywek123 marked this pull request as ready for review October 12, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants