Skip to content

Commit 844e204

Browse files
quic-boyucclaude
andcommitted
qualcomm/observatory: AdbLens groups records under top-level "device" region
Adds the third top-level Region in the typical AOT+device flow: Session "<script-name>" ├── quantization/ (pipeline_graph_collector, AOT) ├── edge/ (pipeline_graph_collector, AOT) └── device/ (this lens; on-device runtime work) ├── adb.execute #1 ├── adb.execute #2 └── ... Region opens lazily on the first patched SimpleADB operation (via `_ensure_device_region`, called from `note_simple_adb`), so a CLI run with no on-device inference doesn't get an empty group cluttering the tree view. The region is closed in `on_session_end` via a `contextlib.ExitStack`. Each inference record (`adb.execute #N`) lives **directly** under `device` — no per-call sub-region — so the region holds multiple records as designed in RFC §4.5. Disabled lens (`config={"adb": {"enabled": False}}`) skips region opening entirely. tests/test_adb_lens_regions.py (new): - 6 tests covering: lazy device-region opening, first SimpleADB call triggers it, multiple records share the one region, disabled-lens does not open it, on_session_end closes the ExitStack, idempotent _ensure_device_region. Verification: source ~/executorch/.venv-11/bin/activate source ~/executorch/qaisw-*/bin/envsetup.sh export QNN_SDK_ROOT=~/executorch/qaisw-v2.41.0.251111144900_191416 PYTHONPATH=~ python -m pytest \ ~/executorch/backends/qualcomm/debugger/observatory/tests/ -v -> 19 passed (6 new + 13 existing AdbLens tests). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 291c072 commit 844e204

2 files changed

Lines changed: 201 additions & 1 deletion

File tree

backends/qualcomm/debugger/observatory/lenses/adb.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import logging
2929
import re
3030
import threading
31+
import contextlib
3132
from dataclasses import dataclass, field
3233
from typing import Any, Callable, Dict, List, Optional
3334

@@ -117,14 +118,34 @@ def _scan_error_lines(text: str) -> List[int]:
117118

118119

119120
class AdbLens(Lens):
120-
"""Lens that records ``SimpleADB`` activity for the run."""
121+
"""Lens that records ``SimpleADB`` activity for the run.
122+
123+
Region structure (cf. RFC §4.5):
124+
125+
Session "<script-name>"
126+
├── quantization/ (pipeline_graph_collector, AOT)
127+
├── edge/ (pipeline_graph_collector, AOT)
128+
└── device/ (this lens; on-device runtime work)
129+
├── adb.execute #1
130+
├── adb.execute #2
131+
└── ...
132+
133+
The ``device`` region is opened lazily on the first patched
134+
``SimpleADB`` operation (push / pull / execute) via
135+
``_ensure_device_region`` and closed in ``on_session_end``. Each
136+
inference record (``adb.execute #N``) lives directly under
137+
``device`` rather than getting its own per-call region — every
138+
region holds more than one Record by design.
139+
"""
121140

122141
_lock = threading.Lock()
123142

124143
_enabled: bool = True
125144
_config: Dict[str, Any] = {}
126145
_patches_installed: bool = False
127146
_install_patches: Optional[Callable[[], None]] = None
147+
_enter_context_fn: Optional[Callable[..., Any]] = None
148+
_device_stack: Optional["contextlib.ExitStack"] = None
128149

129150
_raw_events: List[Dict[str, Any]] = []
130151
_device_info: List[Dict[str, Any]] = []
@@ -175,6 +196,13 @@ def on_session_start(cls, context: ObservationContext) -> Optional[Dict[str, Any
175196
if not cls._enabled:
176197
return {"enabled": False}
177198

199+
# Capture the framework's enter_context callable so the lens can
200+
# lazily open a top-level "device" region on the first patched
201+
# SimpleADB operation. The region is closed in on_session_end.
202+
from executorch.devtools.observatory import Observatory
203+
204+
cls._enter_context_fn = Observatory.enter_context
205+
178206
if cls._install_patches is not None and not cls._patches_installed:
179207
cls._install_patches()
180208
cls._patches_installed = True
@@ -201,6 +229,33 @@ def on_session_end(cls, context: ObservationContext) -> Optional[Dict[str, Any]]
201229
if is_installed():
202230
uninstall_adb_patches()
203231
cls._patches_installed = False
232+
# Close the lazy "device" region (if any) before the framework
233+
# tears down the outer Session. Idempotent: no-op when never
234+
# opened (e.g., a CLI run with no on-device inference).
235+
if cls._device_stack is not None:
236+
try:
237+
cls._device_stack.close()
238+
except Exception as exc:
239+
logging.warning(
240+
"[AdbLens] failed to close device region: %s", exc
241+
)
242+
cls._device_stack = None
243+
cls._enter_context_fn = None
244+
245+
@classmethod
246+
def _ensure_device_region(cls) -> None:
247+
"""Lazy-open the top-level ``device`` Region on first ADB activity.
248+
249+
Idempotent: subsequent calls are no-ops once the region is open.
250+
Called from ``note_simple_adb`` so every patched SimpleADB
251+
operation (push / pull / execute) lands inside the region.
252+
"""
253+
254+
if not cls._enabled or cls._enter_context_fn is None:
255+
return
256+
if cls._device_stack is None:
257+
cls._device_stack = contextlib.ExitStack()
258+
cls._device_stack.enter_context(cls._enter_context_fn("device"))
204259

205260
@classmethod
206261
def observe(cls, artifact: Any, context: ObservationContext) -> Any:
@@ -475,6 +530,9 @@ def _publish_active_inference(cls) -> None:
475530
def note_simple_adb(cls, instance: Any) -> None:
476531
if not cls._enabled:
477532
return
533+
# Open the lazy "device" Region on the first patched ADB call,
534+
# so every record produced from now on is grouped under it.
535+
cls._ensure_device_region()
478536
info = _summarize_simple_adb(instance)
479537
key = (info.get("host"), info.get("device_serial"))
480538
with cls._lock:
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright (c) Qualcomm Innovation Center, Inc.
2+
# All rights reserved
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Unit tests for the AdbLens region structure (RFC §4.5).
8+
9+
The Qualcomm ADB lens contributes a top-level ``device`` Region opened
10+
lazily on the first patched SimpleADB operation. All ``adb.execute #N``
11+
inference records (and any other ADB-related collects) live directly
12+
under it.
13+
14+
These tests don't run an actual on-device inference. They invoke the
15+
public AdbLens entry points (``note_simple_adb``, ``begin_inference``,
16+
``record_event``, ``end_inference``) directly to exercise the region
17+
state machine.
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import pytest
23+
24+
from executorch.backends.qualcomm.debugger.observatory.lenses.adb import (
25+
AdbLens,
26+
AdbExecuteEvent,
27+
)
28+
from executorch.devtools.observatory import Observatory
29+
30+
31+
class _DummyAdb:
32+
"""Minimal stand-in for SimpleADB used by AdbLens.note_simple_adb."""
33+
34+
host_id = "test-host"
35+
device_id = "test-device"
36+
37+
38+
@pytest.fixture(autouse=True)
39+
def _reset_observatory():
40+
Observatory.clear()
41+
Observatory._lens_registry = []
42+
Observatory._lenses_initialized = True
43+
Observatory.register_lens(AdbLens)
44+
yield
45+
# AdbLens has class-level state; reset it so tests are independent.
46+
AdbLens._device_info = []
47+
AdbLens._device_info_seen = set()
48+
AdbLens._raw_events = []
49+
AdbLens._push_groups = []
50+
AdbLens._pull_groups = []
51+
AdbLens._open_groups = []
52+
AdbLens._execute_seq = 0
53+
AdbLens._active_inference = None
54+
AdbLens._device_stack = None
55+
AdbLens._enter_context_fn = None
56+
Observatory.clear()
57+
Observatory._lens_registry = []
58+
Observatory._lenses_initialized = False
59+
60+
61+
def test_no_device_region_until_first_adb_call():
62+
"""The 'device' region is lazy — nothing happens until ADB activity."""
63+
64+
with Observatory.enter_context("session_no_adb"):
65+
# No SimpleADB call yet — collect a record from a hypothetical
66+
# other lens. Its region_stack should not include "device".
67+
Observatory.collect("non_adb_record", object())
68+
69+
rec = list(Observatory._records.values())[0]
70+
assert "device" not in rec.region_stack
71+
assert AdbLens._device_stack is None
72+
73+
74+
def test_first_simple_adb_call_opens_device_region():
75+
"""note_simple_adb fires _ensure_device_region the first time."""
76+
77+
with Observatory.enter_context("session_with_adb"):
78+
AdbLens.note_simple_adb(_DummyAdb())
79+
# Now the device region is open; subsequent collects pick it up.
80+
Observatory.collect("inside_device", object())
81+
82+
rec = list(Observatory._records.values())[0]
83+
assert rec.region_stack == ["session_with_adb", "device"]
84+
85+
86+
def test_multiple_inference_events_share_one_device_region():
87+
"""Several adb.execute records should land directly under 'device' (no
88+
per-call sub-region — every region holds >=2 records)."""
89+
90+
with Observatory.enter_context("session_multi_exec"):
91+
AdbLens.note_simple_adb(_DummyAdb())
92+
# Collect two records from inside the lazy device region. We
93+
# bypass the full inference event flow here because that path
94+
# has many internal dependencies (event publishing, active
95+
# inference accumulator) that aren't worth wiring up for a
96+
# region-structure test.
97+
Observatory.collect("adb.execute #1", object())
98+
Observatory.collect("adb.execute #2", object())
99+
100+
by_region: dict = {}
101+
for rec in Observatory._records.values():
102+
by_region.setdefault(tuple(rec.region_stack), []).append(rec.name)
103+
104+
assert ("session_multi_exec", "device") in by_region
105+
assert len(by_region[("session_multi_exec", "device")]) == 2
106+
107+
108+
def test_disabled_adb_lens_does_not_open_region():
109+
"""When AdbLens is disabled via config, no device region is opened."""
110+
111+
with Observatory.enter_context(
112+
"session_adb_disabled", config={"adb": {"enabled": False}}
113+
):
114+
AdbLens.note_simple_adb(_DummyAdb())
115+
Observatory.collect("non_adb", object())
116+
117+
rec = list(Observatory._records.values())[0]
118+
assert "device" not in rec.region_stack
119+
assert AdbLens._device_stack is None
120+
121+
122+
def test_device_region_closes_on_session_end():
123+
"""on_session_end closes the lazy device ExitStack cleanly."""
124+
125+
with Observatory.enter_context("session_close"):
126+
AdbLens.note_simple_adb(_DummyAdb())
127+
assert AdbLens._device_stack is not None
128+
129+
assert AdbLens._device_stack is None
130+
131+
132+
def test_ensure_device_region_idempotent():
133+
"""Multiple ADB calls do not re-open the region."""
134+
135+
with Observatory.enter_context("session_idempotent"):
136+
AdbLens.note_simple_adb(_DummyAdb())
137+
first = AdbLens._device_stack
138+
AdbLens.note_simple_adb(_DummyAdb())
139+
second = AdbLens._device_stack
140+
141+
# Both reference the same ExitStack (was open already by second call).
142+
assert first is second

0 commit comments

Comments
 (0)