perf: avoid repeated sibling and frame scans during XML serialization - #1283
Merged
Merged
Conversation
Member
Author
|
Maybe this is not so bad |
KazuCocoa
marked this pull request as ready for review
September 29, 2026 06:10
mykola-mokhnach
approved these changes
Sep 29, 2026
github-actions Bot
pushed a commit
that referenced
this pull request
Sep 30, 2026
## [16.13.1](v16.13.0...v16.13.1) (2026-09-30) ### Performance Improvements * reuse sibling indices and geometry during XML serialization ([#1283](#1283)) ([f352660](f352660))
|
🎉 This PR is included in version 16.13.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Serializing each child's index searches its parent's children again, creating quadratic work for wide trees. The four geometry attributes also read and convert the same frame independently.
Pass the known child index through recursive serialization and calculate the rectangle once per serialized snapshot. Preserve the root's existing index behavior. A 64-child regression checks one sibling-list read, one frame read per node, and the resulting XML indices.
Validation: all 178 native unit tests passed on an iOS 26.5 simulator using a local integration branch containing this review series. The regression tests are included in this PR. Native changes were built with Xcode 27.1.
Live WDA
/sourcebenchmarkBoth WDA variants were launched as actual XCTest runners and measured through HTTP, including snapshot acquisition, attribute retrieval, XML generation, and response transfer. Only the production
FBXPath.mimplementation differs; #1276's streaming change and the other optimization PRs are not included.The live endpoint benefit is much smaller than the isolated serializer benchmark. The table reports observed differences, not a demonstrated improvement for every application. Small percentage differences should be treated cautiously given simulator noise and only two WDA launches per variant. These measurements do not separately time snapshot acquisition and serialization, so they cannot identify the precise remaining bottleneck.
The main-screen baseline p95 includes a cluster of slower requests in the second baseline launch. With only two launches per variant, that tail difference cannot confidently be attributed to this change. No slow samples were removed.
Method
261c08d53917b138eaf2f7d8a744d1151fe3bc8d. PR:4fcd3ff36872410468d919e66b109b3913a5dcea./statusbuild timestamps confirmed the intended variant was running.GET /session/{sessionId}/source?format=xml, with no attributes excluded. Time starts before the HTTP request and stops after the complete response body is read. JSON parsing, XML checking, and file saving are outside the timed interval. Startup and session creation/deletion are excluded.bundleId=com.facebook.IntegrationApp,forceAppLaunch=true,shouldTerminateApp=true,shouldWaitForQuiescence=true. Wide fixtures usearguments=["--source-benchmark-nodes", "256"]or"1024"; the main screen uses no arguments.waitForIdleTimeout=10,snapshotMaxDepth=50,snapshotMaxChildren=2147483647,enforceCustomSnapshots=false. Optional hittable/native-frame/native-accessibility/min-max/custom-action page-source attributes were disabled.XML compatibility
All 216 responses (including warmups) succeeded. Within each screen, all outputs matched byte-for-byte after replacing only the application-root
processId, which necessarily changes when the app is relaunched. The comparison required exactly one such attribute per response. No index, geometry, labels, ordering, whitespace, or other XML content was removed or normalized. Measured outputs were 6,174 / 68,450 / 271,858 bytes for the three screens.This supports unchanged XML for these live fixtures and settings. It does not prove regression freedom for every app, configuration, or snapshot shape.
Per-launch medians (ms), in measurement order:
Live benchmark fixture and HTTP driver
The fixture and driver were temporary experiment files, not production changes. Build separate Release WDA runners for the baseline and PR revisions, both with
USE_PORT=8213 USE_IP=127.0.0.1, usingbuild-for-testing. Use separate derived-data paths/private/tmp/wda-live-beforeand/private/tmp/wda-live-after; the driver consumes each generated.xctestrunusingtest-without-building.Example build command (run from the checkout of the relevant revision, changing the derived-data suffix for the variant):
xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner \ -configuration Release -sdk iphonesimulator \ -destination 'platform=iOS Simulator,id=SIMULATOR_UUID' \ -derivedDataPath /private/tmp/wda-live-after build-for-testing \ CODE_SIGNING_ALLOWED=NO USE_PORT=8213 USE_IP=127.0.0.1Apply the fixture patch below to IntegrationApp and build/install that app once for use by both WDA versions. With no launch arguments it uses the unchanged main screen; the driver passes an argument to enable either wide screen.
Build the
IntegrationAppscheme in Release configuration and installIntegrationApp.appwithxcrun simctl install SIMULATOR_UUID /path/to/IntegrationApp.app. Fix the status bar usingxcrun simctl status_bar SIMULATOR_UUID override --time '9:41' --dataNetwork wifi --wifiMode active --wifiBars 3 --batteryState charged --batteryLevel 100.Save the following as
run-block.py. Replace itsudidwith the same simulator UUID and create/private/tmp/wda-pr-artifacts/live-sourcebefore running. Keep the simulator, test app, and build settings unchanged across all four blocks. The script verifies that no other server is using port 8213, starts the chosen WDA build, records all responses and settings, and stops that WDA instance at the end.Run sequentially:
Pool only entries with
warmup=falsefromsamples.jsonfor each variant/screen to compute the median and p95. All original XML files are preserved; comparison normalizes exactly one applicationprocessIdattribute and requires the rest of the bytes to match. The app process ID is the only excluded field. Raw measurements, settings, XML responses, and binary hashes were saved with the experiment artifacts.Performance experiment using the actual WDA serializer
Compared
FBXPath.mfrom the review base (261c08d53917b138eaf2f7d8a744d1151fe3bc8d) with this PR (4fcd3ff3). The benchmark uses the same checkout, test harness, build configuration, and simulator, replacing onlyFBXPath.mbetween runs. No other snapshot optimizations from this review series are included, particularly the XML streaming change in #1276.The synthetic snapshot has one root and 64, 256, or 1,024 direct children. Each child points back to the root, and every node has frame
(1, 2, 3, 4). The query requestsindex,x,y,width, andheight. The harness calls the realxmlRepresentationWithRootElement:...implementation, writes a libxml2 document, dumps it, converts the result to an NSString, and frees the native resources. Timed iterations also drain their autorelease pools. Fixture construction and correctness checks are outside the timed region.Measurement: Release configuration, Xcode 27.1, iOS 26.5 arm64 simulator on macOS 26.6.2. Three warmups followed by 15 measured iterations per size and version, using
CLOCK_MONOTONIC. This is one local comparison, with the PR version measured first and the baseline second; reported values are per-version medians, not an interleaved device benchmark. Read counters are enabled only for a separate counting pass and disabled during timing.The separate counting pass explains the improvement:
childrengetter calls, before → afterframegetter calls, before → afterPreviously, every child's
wdIndexloop repeatedly accessed its parent's child list in both the loop condition and body. Acrossnchildren, this fixture producesn(n+1)child-list accesses, plus one for the serializer's traversal. Passing the traversal index removes those repeated searches. ReusingwdRectalso reduces the four frame reads per node to one; the frame counts include the root.Output verification and scope
Both benchmark runs passed. Every child was checked for the expected index and all four geometry values. The complete serialized XML was captured from both runs and compared byte-for-byte: all three outputs were identical (6,341 / 25,277 / 101,357 bytes).
This measures serialization of already-available synthetic snapshots with the five affected attributes. It excludes real XCTest/AX snapshot acquisition, idle waits, HTTP transport, and other attribute retrieval. The ~11.06× result at 1,024 siblings is not a claim about the full
/sourceendpoint or typical application screens. Wide sibling lists benefit most; trees with few siblings, or queries excluding index/geometry, should benefit less. The two optimizations are measured together, not separately.Reproduce the experiment
In an isolated checkout of PR revision
4fcd3ff3, save the patch below as/tmp/1283-benchmark.patchand apply it withgit apply /tmp/1283-benchmark.patch. It adds an optional benchmark method to the existing test file. The original 64-child regression remains usable. This patch was used only for the experiment; it is not part of the production change.Run the benchmark with an available iOS simulator UUID substituted for
SIMULATOR_UUID:Replace only the serializer with the baseline version, then repeat the same command with the log redirected to
/tmp/1283-before.log:git show 261c08d53917b138eaf2f7d8a744d1151fe3bc8d:WebDriverAgentLib/Utilities/FBXPath.m \ > WebDriverAgentLib/Utilities/FBXPath.mThe logs contain
LINEAR_COUNTS,LINEAR_TIME, and base64-encodedLINEAR_XMLrecords. Confirm both tests succeeded, inspect the counts/times, and compare the exact output bytes: