mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-26 08:08:28 +03:00
Add accessibility performance regression coverage
- Add inactive and active accessibility benchmarks to protect against startup, notification, and AT-SPI scaling regressions. - Extend the AT-SPI benchmark with text, focus, action, value, geometry, hit-testing, and model-update operations. Run both benchmarks in Linux CI and preserve their JSON results as artifacts. - Add an explicit null accessibility backend for baseline measurements and optional accessibility disabling. - Harden the Windows UIA backend by caching client availability per update, validating root providers, and preventing recursive window procedure forwarding.
This commit is contained in:
@@ -22,7 +22,8 @@ jobs:
|
||||
sudo add-apt-repository -y universe
|
||||
sudo add-apt-repository -y multiverse
|
||||
sudo apt update
|
||||
sudo apt install -y gcc-13 g++-13 wget libsdl2-2.0-0 libsdl2-dev mesa-utils xvfb gdb
|
||||
sudo apt install -y gcc-13 g++-13 wget libsdl2-2.0-0 libsdl2-dev mesa-utils xvfb gdb \
|
||||
python3-gi gir1.2-atspi-2.0 at-spi2-core dbus-x11
|
||||
wget https://cdn.ensoft.dev/eepp-assets/premake-5.0.0-beta6-linux.tar.gz
|
||||
tar xvzf premake-5.0.0-beta6-linux.tar.gz
|
||||
- name: Build
|
||||
@@ -74,6 +75,22 @@ jobs:
|
||||
run: |
|
||||
cd projects/scripts
|
||||
bash ./run_gdb_tests.sh
|
||||
- name: Inactive Accessibility Benchmark
|
||||
timeout-minutes: 2
|
||||
run: |
|
||||
mkdir -p bin/unit_tests/output
|
||||
env -u AT_SPI_BUS_ADDRESS dbus-run-session -- projects/scripts/xvfb-run-eepp \
|
||||
/usr/bin/python3 projects/scripts/benchmark_accessibility_inactive.py --json \
|
||||
> bin/unit_tests/output/accessibility-inactive-benchmark.json
|
||||
cat bin/unit_tests/output/accessibility-inactive-benchmark.json
|
||||
- name: AT-SPI Benchmark
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
mkdir -p bin/unit_tests/output
|
||||
env -u AT_SPI_BUS_ADDRESS dbus-run-session -- projects/scripts/xvfb-run-eepp \
|
||||
/usr/bin/python3 projects/scripts/benchmark_atspi.py --json \
|
||||
> bin/unit_tests/output/atspi-benchmark.json
|
||||
cat bin/unit_tests/output/atspi-benchmark.json
|
||||
- name: Upload artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def check(condition, message):
|
||||
if not condition:
|
||||
raise RuntimeError(message)
|
||||
|
||||
|
||||
def run_sample(args, disabled):
|
||||
environment = os.environ.copy()
|
||||
if disabled:
|
||||
environment["EEPP_DISABLE_ACCESSIBILITY"] = "1"
|
||||
else:
|
||||
environment.pop("EEPP_DISABLE_ACCESSIBILITY", None)
|
||||
start = time.perf_counter()
|
||||
completed = subprocess.run(
|
||||
[
|
||||
args.executable,
|
||||
"--benchmark-inactive",
|
||||
"--benchmark-iterations",
|
||||
str(args.iterations),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=args.timeout,
|
||||
env=environment,
|
||||
)
|
||||
wall_ms = (time.perf_counter() - start) * 1000
|
||||
result = json.loads(completed.stdout)
|
||||
result["wall_ms"] = wall_ms
|
||||
result["initialization_ms"] = result.pop("initialization_us") / 1000
|
||||
result["notifications_ms"] = result.pop("notifications_us") / 1000
|
||||
return result
|
||||
|
||||
|
||||
def median_sample(samples):
|
||||
return {
|
||||
key: statistics.median(sample[key] for sample in samples)
|
||||
for key in samples[0]
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Benchmark inactive accessibility overhead")
|
||||
parser.add_argument("--executable", default="bin/eepp-ui-accessibility")
|
||||
parser.add_argument("--iterations", type=int, default=100000)
|
||||
parser.add_argument("--samples", type=int, default=5)
|
||||
parser.add_argument("--timeout", type=float, default=10)
|
||||
parser.add_argument("--max-initialization-ms", type=float, default=250)
|
||||
parser.add_argument("--max-notification-ratio", type=float, default=1.1)
|
||||
parser.add_argument("--max-wall-ratio", type=float, default=1.25)
|
||||
parser.add_argument("--json", action="store_true")
|
||||
args = parser.parse_args()
|
||||
check(args.iterations > 0 and args.samples > 0, "iterations and samples must be positive")
|
||||
|
||||
# Alternate modes so thermal and scheduler drift affect both populations similarly.
|
||||
enabled_samples = []
|
||||
disabled_samples = []
|
||||
for index in range(args.samples * 2):
|
||||
disabled = index % 2 == 0
|
||||
result = run_sample(args, disabled)
|
||||
(disabled_samples if disabled else enabled_samples).append(result)
|
||||
|
||||
enabled = median_sample(enabled_samples)
|
||||
disabled = median_sample(disabled_samples)
|
||||
notification_ratio = enabled["notifications_ms"] / disabled["notifications_ms"]
|
||||
wall_ratio = enabled["wall_ms"] / disabled["wall_ms"]
|
||||
check(
|
||||
enabled["initialization_ms"] <= args.max_initialization_ms,
|
||||
"inactive native backend initialization exceeded threshold",
|
||||
)
|
||||
check(
|
||||
notification_ratio <= args.max_notification_ratio,
|
||||
"inactive accessibility notification overhead exceeded threshold",
|
||||
)
|
||||
check(wall_ratio <= args.max_wall_ratio, "inactive accessibility wall-time overhead exceeded threshold")
|
||||
output = {
|
||||
"disabled": disabled,
|
||||
"enabled": enabled,
|
||||
"notification_ratio": notification_ratio,
|
||||
"wall_ratio": wall_ratio,
|
||||
}
|
||||
if args.json:
|
||||
print(json.dumps(output, sort_keys=True))
|
||||
else:
|
||||
print(
|
||||
"Inactive accessibility benchmark passed: "
|
||||
f"initialization {enabled['initialization_ms']:.3f}ms, "
|
||||
f"notifications {enabled['notifications_ms']:.3f}ms "
|
||||
f"({notification_ratio:.3f}x disabled), wall {wall_ratio:.3f}x disabled"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -92,19 +92,172 @@ def action_index(node, name):
|
||||
raise RuntimeError(f'action "{name}" was not exposed by "{node.get_name()}"')
|
||||
|
||||
|
||||
def run_sample(args, item_count, target_process_id, model_changed, model_change_count):
|
||||
process = None
|
||||
try:
|
||||
start = time.perf_counter()
|
||||
process = subprocess.Popen(
|
||||
[args.executable, "--benchmark", "--benchmark-items", str(item_count)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
target_process_id[0] = process.pid
|
||||
application = find_application(args.application, process.pid, args.timeout)
|
||||
discovery_ms = (time.perf_counter() - start) * 1000
|
||||
|
||||
measurements = Measurements()
|
||||
start = time.perf_counter()
|
||||
nodes = traverse(application, measurements)
|
||||
traversal_ms = (time.perf_counter() - start) * 1000
|
||||
check(len(nodes) >= item_count + 50, f"benchmark tree is incomplete: {len(nodes)} nodes")
|
||||
find_named(nodes, "Benchmark depth leaf")
|
||||
benchmark_list = find_named(nodes, "Benchmark items")
|
||||
check(
|
||||
benchmark_list.get_child_count() == item_count,
|
||||
f"benchmark list must expose {item_count} items",
|
||||
)
|
||||
|
||||
start = time.perf_counter()
|
||||
for node in nodes[: min(args.sample_size, len(nodes))]:
|
||||
measurements.call("description", node.get_description)
|
||||
measurements.call("state", node.get_state_set)
|
||||
measurements.call("attributes", node.get_attributes)
|
||||
measurements.call("extents", lambda node=node: node.get_extents(Atspi.CoordType.SCREEN))
|
||||
property_batch_ms = (time.perf_counter() - start) * 1000
|
||||
|
||||
project_name = find_named(nodes, "Project name")
|
||||
check(
|
||||
measurements.call("text-character-count", project_name.get_character_count) == 4,
|
||||
"text character count query failed",
|
||||
)
|
||||
check(
|
||||
measurements.call(
|
||||
"text-content", lambda: Atspi.Text.get_text(project_name, 0, -1)
|
||||
) == "eepp",
|
||||
"text content query failed",
|
||||
)
|
||||
check(
|
||||
measurements.call(
|
||||
"editable-text", lambda: Atspi.EditableText.set_text_contents(project_name, "eepp2")
|
||||
),
|
||||
"editable text action failed",
|
||||
)
|
||||
check(
|
||||
measurements.call(
|
||||
"focus", lambda: project_name.do_action(action_index(project_name, "focus"))
|
||||
),
|
||||
"focus action failed",
|
||||
)
|
||||
|
||||
checkbox = find_named(nodes, "Enable autosave")
|
||||
check(
|
||||
measurements.call(
|
||||
"toggle", lambda: checkbox.do_action(action_index(checkbox, "toggle"))
|
||||
),
|
||||
"toggle action failed",
|
||||
)
|
||||
check(
|
||||
measurements.call("toggle-state", checkbox.get_state_set).contains(
|
||||
Atspi.StateType.CHECKED
|
||||
),
|
||||
"toggle state query failed",
|
||||
)
|
||||
|
||||
spin = find_named(nodes, "Retry count")
|
||||
check(measurements.call("range-value", spin.get_current_value) == 3, "range value query failed")
|
||||
check(
|
||||
measurements.call(
|
||||
"set-range-value", lambda: Atspi.Value.set_current_value(spin, 4)
|
||||
),
|
||||
"range value action failed",
|
||||
)
|
||||
|
||||
window = measurements.call("application-window", lambda: application.get_child_at_index(0))
|
||||
window_extents = measurements.call(
|
||||
"window-extents", lambda: window.get_extents(Atspi.CoordType.SCREEN)
|
||||
)
|
||||
hit = measurements.call(
|
||||
"component-hit-test",
|
||||
lambda: window.get_accessible_at_point(
|
||||
window_extents.x + window_extents.width // 2,
|
||||
window_extents.y + window_extents.height // 2,
|
||||
Atspi.CoordType.SCREEN,
|
||||
),
|
||||
)
|
||||
check(hit is not None, "component hit test failed")
|
||||
|
||||
mutate = find_named(nodes, "Replace benchmark model")
|
||||
model_changed.clear()
|
||||
model_change_count[0] = 0
|
||||
start = time.perf_counter()
|
||||
check(
|
||||
mutate.do_action(action_index(mutate, "click")),
|
||||
"benchmark model replacement action failed",
|
||||
)
|
||||
check(model_changed.wait(args.timeout), "model replacement event was not received")
|
||||
while benchmark_list.get_child_count() != item_count and time.monotonic() - start < args.timeout:
|
||||
time.sleep(0.001)
|
||||
mutation_ms = (time.perf_counter() - start) * 1000
|
||||
first_item = benchmark_list.get_child_at_index(0)
|
||||
check(first_item.get_name() == "Updated benchmark item 0", "model replacement stayed stale")
|
||||
check(model_change_count[0] == 1, "model-change burst was not coalesced")
|
||||
|
||||
return {
|
||||
"nodes": len(nodes),
|
||||
"item_count": item_count,
|
||||
"discovery_ms": discovery_ms,
|
||||
"traversal_ms": traversal_ms,
|
||||
"property_batch_ms": property_batch_ms,
|
||||
"mutation_ms": mutation_ms,
|
||||
"model_change_events": model_change_count[0],
|
||||
**measurements.summary(),
|
||||
}
|
||||
finally:
|
||||
target_process_id[0] = None
|
||||
if process:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=1)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
|
||||
|
||||
def median_result(samples):
|
||||
result = {"item_count": samples[0]["item_count"], "iterations": len(samples)}
|
||||
for key in samples[0]:
|
||||
if key not in {"item_count", "model_change_events"}:
|
||||
result[key] = statistics.median(sample[key] for sample in samples)
|
||||
result["model_change_events"] = max(sample["model_change_events"] for sample in samples)
|
||||
result["traversal_ms_per_1k"] = result["traversal_ms"] * 1000 / result["nodes"]
|
||||
result["mutation_ms_per_1k"] = result["mutation_ms"] * 1000 / result["item_count"]
|
||||
return result
|
||||
|
||||
|
||||
def parse_item_counts(value):
|
||||
counts = sorted(set(int(count) for count in value.split(",")))
|
||||
check(counts and counts[0] > 0, "item counts must be positive")
|
||||
return counts
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Benchmark eepp's AT-SPI query path")
|
||||
parser.add_argument("--application", default="eepp - Accessibility")
|
||||
parser.add_argument("--executable", default="bin/eepp-ui-accessibility")
|
||||
parser.add_argument("--timeout", type=float, default=5.0)
|
||||
parser.add_argument("--sample-size", type=int, default=100)
|
||||
parser.add_argument("--max-discovery-ms", type=float, default=1000)
|
||||
parser.add_argument("--max-traversal-ms", type=float, default=1500)
|
||||
parser.add_argument("--max-property-batch-ms", type=float, default=250)
|
||||
parser.add_argument("--max-mutation-ms", type=float, default=250)
|
||||
parser.add_argument("--max-request-ms", type=float, default=100)
|
||||
parser.add_argument("--item-counts", default="250,1000,4000")
|
||||
parser.add_argument("--iterations", type=int, default=3)
|
||||
parser.add_argument("--max-discovery-ms", type=float, default=2000)
|
||||
parser.add_argument("--max-traversal-ms-per-1k", type=float, default=2000)
|
||||
parser.add_argument("--max-property-batch-ms", type=float, default=500)
|
||||
parser.add_argument("--max-mutation-ms-per-1k", type=float, default=1000)
|
||||
parser.add_argument("--max-request-ms", type=float, default=250)
|
||||
parser.add_argument("--max-p95-ms", type=float, default=5)
|
||||
parser.add_argument("--max-scaling-ratio", type=float, default=2.5)
|
||||
parser.add_argument("--json", action="store_true")
|
||||
args = parser.parse_args()
|
||||
check(args.iterations > 0, "iterations must be positive")
|
||||
item_counts = parse_item_counts(args.item_counts)
|
||||
|
||||
model_changed = threading.Event()
|
||||
model_change_count = [0]
|
||||
@@ -123,86 +276,52 @@ def main():
|
||||
event_thread = threading.Thread(target=Atspi.event_main, daemon=True)
|
||||
event_thread.start()
|
||||
|
||||
process = None
|
||||
try:
|
||||
start = time.perf_counter()
|
||||
process = subprocess.Popen([args.executable, "--benchmark"])
|
||||
target_process_id[0] = process.pid
|
||||
application = find_application(args.application, process.pid, args.timeout)
|
||||
discovery_ms = (time.perf_counter() - start) * 1000
|
||||
results = []
|
||||
for item_count in item_counts:
|
||||
samples = [
|
||||
run_sample(
|
||||
args, item_count, target_process_id, model_changed, model_change_count
|
||||
)
|
||||
for _ in range(args.iterations)
|
||||
]
|
||||
results.append(median_result(samples))
|
||||
|
||||
measurements = Measurements()
|
||||
start = time.perf_counter()
|
||||
nodes = traverse(application, measurements)
|
||||
traversal_ms = (time.perf_counter() - start) * 1000
|
||||
check(len(nodes) >= 1050, f"benchmark tree is incomplete: {len(nodes)} nodes")
|
||||
find_named(nodes, "Benchmark depth leaf")
|
||||
benchmark_list = find_named(nodes, "Benchmark items")
|
||||
check(benchmark_list.get_child_count() == 1000, "benchmark list must expose 1000 items")
|
||||
|
||||
start = time.perf_counter()
|
||||
for node in nodes[: args.sample_size]:
|
||||
measurements.call("description", node.get_description)
|
||||
measurements.call("state", node.get_state_set)
|
||||
measurements.call("attributes", node.get_attributes)
|
||||
measurements.call("extents", lambda node=node: node.get_extents(Atspi.CoordType.SCREEN))
|
||||
property_batch_ms = (time.perf_counter() - start) * 1000
|
||||
|
||||
mutate = find_named(nodes, "Replace benchmark model")
|
||||
model_changed.clear()
|
||||
start = time.perf_counter()
|
||||
check(
|
||||
mutate.do_action(action_index(mutate, "click")),
|
||||
"benchmark model replacement action failed",
|
||||
)
|
||||
check(model_changed.wait(args.timeout), "model replacement event was not received")
|
||||
while benchmark_list.get_child_count() != 1000 and time.monotonic() - start < args.timeout:
|
||||
time.sleep(0.001)
|
||||
mutation_ms = (time.perf_counter() - start) * 1000
|
||||
first_item = benchmark_list.get_child_at_index(0)
|
||||
check(first_item.get_name() == "Updated benchmark item 0", "model replacement stayed stale")
|
||||
check(model_change_count[0] == 1, "model-change burst was not coalesced")
|
||||
|
||||
request_summary = measurements.summary()
|
||||
result = {
|
||||
"nodes": len(nodes),
|
||||
"discovery_ms": discovery_ms,
|
||||
"traversal_ms": traversal_ms,
|
||||
"property_batch_ms": property_batch_ms,
|
||||
"mutation_ms": mutation_ms,
|
||||
"model_change_events": model_change_count[0],
|
||||
**request_summary,
|
||||
}
|
||||
check(discovery_ms <= args.max_discovery_ms, "application discovery exceeded threshold")
|
||||
check(traversal_ms <= args.max_traversal_ms, "tree traversal exceeded threshold")
|
||||
check(property_batch_ms <= args.max_property_batch_ms, "property batch exceeded threshold")
|
||||
check(mutation_ms <= args.max_mutation_ms, "model mutation exceeded threshold")
|
||||
check(
|
||||
request_summary["maximum_ms"] <= args.max_request_ms,
|
||||
"an individual accessibility request exceeded threshold",
|
||||
)
|
||||
if args.json:
|
||||
print(json.dumps(result, sort_keys=True))
|
||||
else:
|
||||
print(
|
||||
"AT-SPI benchmark passed: "
|
||||
f"{result['nodes']} nodes, {result['calls']} calls, "
|
||||
f"discovery {discovery_ms:.1f}ms, traversal {traversal_ms:.1f}ms, "
|
||||
f"properties {property_batch_ms:.1f}ms, mutation {mutation_ms:.1f}ms, "
|
||||
f"median {result['median_ms']:.3f}ms, p95 {result['p95_ms']:.3f}ms, "
|
||||
f"maximum {result['maximum_ms']:.3f}ms"
|
||||
for result in results:
|
||||
check(result["discovery_ms"] <= args.max_discovery_ms, "application discovery exceeded threshold")
|
||||
check(
|
||||
result["traversal_ms_per_1k"] <= args.max_traversal_ms_per_1k,
|
||||
"normalized tree traversal exceeded threshold",
|
||||
)
|
||||
check(result["property_batch_ms"] <= args.max_property_batch_ms, "property batch exceeded threshold")
|
||||
check(
|
||||
result["mutation_ms_per_1k"] <= args.max_mutation_ms_per_1k,
|
||||
"normalized model mutation exceeded threshold",
|
||||
)
|
||||
check(result["maximum_ms"] <= args.max_request_ms, "an individual accessibility request exceeded threshold")
|
||||
check(result["p95_ms"] <= args.max_p95_ms, "p95 accessibility request latency exceeded threshold")
|
||||
|
||||
baseline_cost = results[0]["traversal_ms_per_1k"]
|
||||
scaling_ratio = max(
|
||||
result["traversal_ms_per_1k"] / baseline_cost for result in results[1:]
|
||||
) if len(results) > 1 else 1
|
||||
check(scaling_ratio <= args.max_scaling_ratio, "tree traversal scaling became nonlinear")
|
||||
output = {"worst_scaling_ratio": scaling_ratio, "samples": results}
|
||||
if args.json:
|
||||
print(json.dumps(output, sort_keys=True))
|
||||
else:
|
||||
for result in results:
|
||||
print(
|
||||
f"{result['item_count']} items: {result['nodes']:.0f} nodes, "
|
||||
f"traversal {result['traversal_ms']:.1f}ms "
|
||||
f"({result['traversal_ms_per_1k']:.1f}ms/1k), "
|
||||
f"p95 {result['p95_ms']:.3f}ms, maximum {result['maximum_ms']:.3f}ms"
|
||||
)
|
||||
print(f"AT-SPI benchmark passed: worst normalized growth {scaling_ratio:.2f}x")
|
||||
finally:
|
||||
listener.deregister("object:model-changed")
|
||||
Atspi.event_quit()
|
||||
event_thread.join(timeout=1)
|
||||
if process:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=1)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -9,10 +9,14 @@ class NullAccessibilityBackend final : public AccessibilityBackend {
|
||||
void onEvent( const AccessibilityPendingEvent& ) {}
|
||||
};
|
||||
|
||||
std::unique_ptr<AccessibilityBackend> createNullAccessibilityBackend() {
|
||||
return std::make_unique<NullAccessibilityBackend>();
|
||||
}
|
||||
|
||||
#if EE_PLATFORM != EE_PLATFORM_LINUX && EE_PLATFORM != EE_PLATFORM_FREEBSD && \
|
||||
EE_PLATFORM != EE_PLATFORM_WIN
|
||||
std::unique_ptr<AccessibilityBackend> createAccessibilityBackend( AccessibilityManager& ) {
|
||||
return std::make_unique<NullAccessibilityBackend>();
|
||||
return createNullAccessibilityBackend();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ class AccessibilityBackend {
|
||||
|
||||
std::unique_ptr<AccessibilityBackend> createAccessibilityBackend( AccessibilityManager& manager );
|
||||
|
||||
std::unique_ptr<AccessibilityBackend> createNullAccessibilityBackend();
|
||||
|
||||
}} // namespace EE::UI
|
||||
|
||||
#endif
|
||||
|
||||
@@ -225,7 +225,8 @@ class AtSpiApplication final : public std::enable_shared_from_this<AtSpiApplicat
|
||||
mRunning.store( false, std::memory_order_release );
|
||||
if ( mIOThread.joinable() ) {
|
||||
const char stop = 1;
|
||||
write( mWakePipe[1], &stop, sizeof( stop ) );
|
||||
const ssize_t written = write( mWakePipe[1], &stop, sizeof( stop ) );
|
||||
(void)written;
|
||||
mIOThread.join();
|
||||
close( mWakePipe[0] );
|
||||
close( mWakePipe[1] );
|
||||
|
||||
@@ -24,7 +24,9 @@ class UIAutomationAccessibilityBackend final : public AccessibilityBackend {
|
||||
|
||||
bool isAvailable() const { return mWindow != nullptr; }
|
||||
|
||||
bool hasActiveClients() const { return isAvailable() && UiaClientsAreListening(); }
|
||||
bool hasActiveClients() const { return isAvailable() && mHasActiveClients; }
|
||||
|
||||
void update() { mHasActiveClients = UiaClientsAreListening(); }
|
||||
|
||||
void onEvent( const AccessibilityPendingEvent& event );
|
||||
|
||||
@@ -50,6 +52,7 @@ class UIAutomationAccessibilityBackend final : public AccessibilityBackend {
|
||||
AccessibilityManager& mManager;
|
||||
HWND mWindow{};
|
||||
WNDPROC mPreviousWindowProcedure{};
|
||||
bool mHasActiveClients{};
|
||||
UnorderedMap<AccessibilitySourceId, UnorderedMap<Uint64, UIAutomationProvider*>> mProviders;
|
||||
};
|
||||
|
||||
@@ -593,14 +596,17 @@ LRESULT CALLBACK UIAutomationAccessibilityBackend::windowProcedure( HWND window,
|
||||
static_cast<UIAutomationAccessibilityBackend*>( GetPropW( window, BackendProperty ) );
|
||||
if ( backend && message == WM_GETOBJECT && static_cast<LONG>( lParam ) == UiaRootObjectId ) {
|
||||
auto root = backend->provider( backend->manager().getRoot() );
|
||||
if ( !root )
|
||||
return DefWindowProcW( window, message, wParam, lParam );
|
||||
LRESULT result = UiaReturnRawElementProvider(
|
||||
window, wParam, lParam, static_cast<IRawElementProviderSimple*>( root ) );
|
||||
root->Release();
|
||||
return result;
|
||||
}
|
||||
return backend ? CallWindowProcW( backend->mPreviousWindowProcedure, window, message, wParam,
|
||||
lParam )
|
||||
: DefWindowProcW( window, message, wParam, lParam );
|
||||
WNDPROC previous = backend ? backend->mPreviousWindowProcedure : nullptr;
|
||||
return previous && previous != &UIAutomationAccessibilityBackend::windowProcedure
|
||||
? CallWindowProcW( previous, window, message, wParam, lParam )
|
||||
: DefWindowProcW( window, message, wParam, lParam );
|
||||
}
|
||||
|
||||
UIAutomationProvider* UIAutomationAccessibilityBackend::provider( AccessibilityNodeRef ref ) {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <eepp/ui/uiscenenode.hpp>
|
||||
#include <eepp/ui/uiwidget.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace EE { namespace UI {
|
||||
|
||||
namespace {
|
||||
@@ -262,8 +264,11 @@ bool AccessibilityManager::hasActiveNativeClients() const {
|
||||
}
|
||||
|
||||
void AccessibilityManager::update() {
|
||||
if ( !mBackend )
|
||||
mBackend = createAccessibilityBackend( *this );
|
||||
if ( !mBackend ) {
|
||||
mBackend = std::getenv( "EEPP_DISABLE_ACCESSIBILITY" )
|
||||
? createNullAccessibilityBackend()
|
||||
: createAccessibilityBackend( *this );
|
||||
}
|
||||
if ( mBackend ) {
|
||||
mBackend->update();
|
||||
mPendingEvents.clear();
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
#include <eepp/ee.hpp>
|
||||
#include <eepp/ui/accessibility/accessibilitymanager.hpp>
|
||||
#include <eepp/ui/tools/uiwidgetinspector.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
EE_MAIN_FUNC int main( int argc, char** argv ) {
|
||||
bool multiWindow = false;
|
||||
bool closePrimary = false;
|
||||
bool benchmark = false;
|
||||
bool benchmarkInactive = false;
|
||||
size_t benchmarkItems = 1000;
|
||||
size_t benchmarkIterations = 100000;
|
||||
for ( int i = 1; i < argc; ++i ) {
|
||||
multiWindow |= std::string_view( argv[i] ) == "--multi-window";
|
||||
closePrimary |= std::string_view( argv[i] ) == "--close-primary";
|
||||
benchmark |= std::string_view( argv[i] ) == "--benchmark";
|
||||
benchmarkInactive |= std::string_view( argv[i] ) == "--benchmark-inactive";
|
||||
if ( std::string_view( argv[i] ) == "--benchmark-items" && i + 1 < argc )
|
||||
benchmarkItems = std::max<size_t>( 1, std::strtoull( argv[++i], nullptr, 10 ) );
|
||||
if ( std::string_view( argv[i] ) == "--benchmark-iterations" && i + 1 < argc )
|
||||
benchmarkIterations = std::max<size_t>( 1, std::strtoull( argv[++i], nullptr, 10 ) );
|
||||
}
|
||||
UIApplication app( { 1280, 720, "eepp - Accessibility",
|
||||
static_cast<Uint32>( benchmark ? WindowStyle::Default | WindowStyle::Hidden
|
||||
: WindowStyle::Default ) } );
|
||||
static_cast<Uint32>( benchmark || benchmarkInactive
|
||||
? WindowStyle::Default | WindowStyle::Hidden
|
||||
: WindowStyle::Default ) } );
|
||||
multiWindow |= closePrimary;
|
||||
if ( closePrimary )
|
||||
app.setQuitPolicy( UIApplication::QuitPolicy::OnLastWindowClosed );
|
||||
@@ -59,8 +72,8 @@ EE_MAIN_FUNC int main( int argc, char** argv ) {
|
||||
auto list = content->find<UIListView>( "items" );
|
||||
if ( benchmark ) {
|
||||
std::vector<std::string> items;
|
||||
items.reserve( 1000 );
|
||||
for ( size_t i = 0; i < 1000; ++i )
|
||||
items.reserve( benchmarkItems );
|
||||
for ( size_t i = 0; i < benchmarkItems; ++i )
|
||||
items.emplace_back( "Benchmark item " + std::to_string( i ) );
|
||||
list->setAccessibilityLabel( "Benchmark items" );
|
||||
list->setModel( Models::ItemListOwnerModel<std::string>::create( std::move( items ) ) );
|
||||
@@ -76,6 +89,19 @@ EE_MAIN_FUNC int main( int argc, char** argv ) {
|
||||
table->setModel( tableModel );
|
||||
|
||||
auto status = content->find<UITextView>( "status" );
|
||||
if ( benchmarkInactive ) {
|
||||
Clock clock;
|
||||
scene->getAccessibilityManager()->update();
|
||||
const auto initializationUs = clock.getElapsedTime().asMicroseconds();
|
||||
clock.restart();
|
||||
for ( size_t i = 0; i < benchmarkIterations; ++i )
|
||||
status->setText( i & 1 ? "Ready" : "Idle" );
|
||||
const auto notificationsUs = clock.getElapsedTime().asMicroseconds();
|
||||
std::cout << "{\"initialization_us\":" << initializationUs
|
||||
<< ",\"iterations\":" << benchmarkIterations
|
||||
<< ",\"notifications_us\":" << notificationsUs << "}\n";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
content->find<UIPushButton>( "save" )->onClick(
|
||||
[status]( auto ) { status->setText( "Settings saved" ); } );
|
||||
content->find<UIPushButton>( "inspect" )->onClick( [scene]( auto ) {
|
||||
@@ -115,11 +141,11 @@ EE_MAIN_FUNC int main( int argc, char** argv ) {
|
||||
auto mutate = UIPushButton::New();
|
||||
mutate->setText( "Replace benchmark model" );
|
||||
mutate->setParent( content );
|
||||
mutate->onClick( [list]( auto ) {
|
||||
mutate->onClick( [list, benchmarkItems]( auto ) {
|
||||
for ( size_t generation = 0; generation < 16; ++generation ) {
|
||||
std::vector<std::string> items;
|
||||
items.reserve( 1000 );
|
||||
for ( size_t i = 0; i < 1000; ++i )
|
||||
items.reserve( benchmarkItems );
|
||||
for ( size_t i = 0; i < benchmarkItems; ++i )
|
||||
items.emplace_back( "Updated benchmark item " + std::to_string( i ) );
|
||||
list->setModel(
|
||||
Models::ItemListOwnerModel<std::string>::create( std::move( items ) ) );
|
||||
|
||||
Reference in New Issue
Block a user