Conversation
65281e8 to
c750ece
Compare
|
Thank you for this contribution! We will be able to pick this up for review the week of September 21 or 28. |
c750ece to
cad8085
Compare
|
@juliasilge that would be this or next week, right? |
juliasilge
left a comment
There was a problem hiding this comment.
Thank you so much for this contribution, @kv9898! 🙌
Before we look at small details, I want to talk about three problems with the overall design. Each problem affects the shape of the comm methods, so we'll need to get the design right there, before implementing the Ark and Python sides of the comms.
1. Python autocomplete imports every installed package into the user's session
get_help_topics() calls pydoc.ModuleScanner().run(callback, "", ...). Internally, ModuleScanner uses pkgutil.walk_packages(), which must import each package to find its submodules. The docstring of walk_packages says this:
Note that this function must import all packages (NOT all modules!) on the given path, in order to access the
__path__attribute to find submodules.
The scan runs in the kernel process, so all of these imports go into the user's sys.modules. The __init__ of each package runs, with all of its side effects (for example, plugin registration, logging handlers, or GPU setup). Some packages also print to stdout during import. Also, because the key is "" and not None, ModuleScanner reads the source of every module and imports all built-in modules.
I measured the same scan in some local environments:
| Environment | Topics | Time | New entries in sys.modules |
|---|---|---|---|
| Base Python | 2,181 | 1.6s | 640 |
| Data science environment | 27,183 | 33.3s | 7,977 |
| Older data science environment | 17,051 | 37.7s | 10,253 |
The pydoc search?key= page that search_help() opens does a similar scan with the same imports, on the pydoc server thread.
Some options that do not import user packages:
- Use
pkgutil.iter_modules()for top-level names only. This function does not import anything, and it's fast. - Find submodules from the file system. Get each package location from
importlib.util.find_spec(), then callpkgutil.iter_modules([location], prefix)on that path. - Do the full scan in a subprocess of the same interpreter, in the background, and cache the result.
2. Slow work runs on the interpreter's own thread, with the default 5-second timeout
Both backends do expensive work synchronously on the thread that runs user code. In Ark, the comm handlers run on the R thread, and help.search() builds the search database on its first call. In Python, the scan above runs when the kernel handles the comm message. As a result, help requests wait behind any user code that is running, and user code waits behind the help requests.
PositronHelpComm is created without RPC options, so search_help and get_help_topics use the default timeout of 5 seconds from PositronBaseComm. Thus:
- If the first search in a session takes more than 5 seconds, the user sees "An error occurred while searching help". Then the results page opens anyway when the backend finishes.
- If the topic list takes more than 5 seconds, the error is not shown and no suggestions appear.
- If R or Python is busy with a long computation, every help request times out.
The design needs to keep this work off the path of user code, or at least make it cheap and give the methods an explicit timeout that matches the real cost.
3. The frontend receives the full topic list and filters it on every keystroke
get_help_topics returns every topic in one response. For R, Ark returns one pkg::alias entry for each alias. With a few hundred packages installed, this is approximately 100k entries. The component then lowercases, filters, and sorts the full list on each keystroke, and only after that takes the first 50 results. This will cause input lag in large environments. The component also caches the full list in a useRef Map inside the React component, with no invalidation when a user installs a package.
I think a query-based method is a better design. For example, the frontend sends the current text, and the backend returns the best N matches. This is the same model that console completions use. With this design, the full list does not go over the comm, the frontend does not need a large cache, and the backend can use its own index. Newly installed packages are also found, because each query reads the current state.
I am happy to answer questions about any of these points. Thank you again for your work on this!
|
I found this thread while looking at the help experience in arf console. The concerns here about doing help indexing/search on the R thread are very similar to problems we've been working on in arf. arf uses r-documentation-rs to read installed R documentation metadata directly from Rust, build a lightweight index, and only load the selected help page afterward. For reference, in one benchmark over 130 installed packages / 6,693 entries, arf built its help index in about 25 ms (eitsupi/arf#375). r-documentation-rs already reads Since Ark and arf need much of the same low-level handling of installed R documentation, I wonder if sharing this infrastructure through r-documentation-rs could be useful for both projects, rather than maintaining separate implementations. Just wanted to mention the existing arf implementation in case it is useful while reconsidering the help-search design here. |
|
Thanks @eitsupi, this looks relevant to the R-side indexing work. @juliasilge, would you be comfortable with me trying Before integrating it, I’d check search coverage against the current This would move expensive indexing off the R thread; it wouldn’t by itself make Help comm requests work while R is busy. For this revision, I propose handling busy sessions explicitly in the UI rather than broadening the work into independent request dispatch. Does that sound like a reasonable direction to explore? |
|
@juliasilge @eitsupi, a follow-up on scope: I plan to keep the current Ark PR (#1401) free of a new On Python, I’m proceeding with import-free static discovery supplemented by a snapshot of already-loaded module metadata. This preserves discoverable module names and available summaries without importing packages to populate suggestions or run broad searches. I’ll document the remaining coverage limitation: runtime-only summaries and dynamically exposed paths of unloaded packages may be absent. Explicitly opening a selected topic will retain the existing help-resolution behavior. I’ll bring the concrete shared protocol proposal here before wiring the revised backends and frontend together. |
|
Scope note: this work also encounters the existing same-URL Help refresh behavior tracked in #4484 (successive R searches such as |
|
@juliasilge, I have revised this PR and the companion Ark PR to address your three points: 1. Python discovery importing installed packagesBoth autocomplete and the pydoc search-results route now use a shared index built from static filesystem/ZIP discovery plus a snapshot of already-loaded module metadata. Discovery no longer uses The tradeoff is that runtime-only summaries and dynamically exposed paths of unloaded packages may be absent. Tests cover discovery without executing package initializers, including the HTTP search route. 2. Synchronous work, busy interpreters, and timeoutsThe frontend waits for an idle/ready interpreter before dispatching suggestions or a submitted search, and shows a waiting status for submitted searches. Both Help RPCs now have an explicit 30-second timeout. Submitted searches also have a 30-second waiting deadline, reset when dispatched.
Indexing is cached, but first construction/rebuild still runs synchronously. This revision does not introduce background indexing or independent dispatch while R is busy. Ark retains native 3. Full topic catalogue and cache invalidation
The backend indexes check freshness: R tracks library paths and installed documentation metadata; Python tracks search paths, loaded-module metadata, and filesystem/ZIP metadata. Relevant changes rebuild the index, including additions/removals and documentation updates. Validation: 21 frontend tests passed; Python Help/index/pydoc tests passed (122 passed, 1 skipped); the 23 selected Ark Help tests passed across the initial run and corrected rerun. Frontend typechecking, client transpilation, R/Python extension compilation, and the Ark debug build passed. I have also tried the revised development build manually and it works well locally. As noted separately, #4484 remains unchanged and is left to the Positron team. The frontend and both backends need to land together because the Help request signatures changed. |
|
@juliasilge @eitsupi, I have an optional It is a draft PR in my fork, targeting I also benchmarked the current R-only implementation (
Method: Linux laptop (Core Ultra 9 285H), R 4.6.1, 340 installed package entries; optimized build, median of 15 samples per implementation/scenario. Suggestions used The result is a tradeoff: about 29% faster first-use suggestions, but slower initial searches. The baseline prepares R’s native search database while constructing suggestions; the prototype reads aliases separately, so the first submitted search pays the deferred native-database build. A fresh submitted search pays for both paths. Cached suggestions and repeated searches were essentially unchanged. These are measurements of this prototype on one machine, not a general performance claim about the library. The benchmark verified identical bounded suggestions and that the Rust reader succeeded without taking the fallback. The prototype also passed 25 targeted Help tests and Clippy. Indexing still runs synchronously; this draft does not yet implement the background worker discussed earlier. I would welcome feedback on the reader adapter and index lifecycle before taking that further. The existing upstream PRs remain independent of this experiment. |
juliasilge
left a comment
There was a problem hiding this comment.
Thank you for this revision, @kv9898! The design is in a much better place now. Python no longer imports packages to find topics, suggestions are query-based and bounded, and the help RPCs have explicit timeouts. I ran the new Python index in my local environments, and the only new entries in sys.modules were codec modules. 🎉
Here are the next steps that I see.
1. Do not add r-documentation-rs to Ark
Thank you for the prototype in kv9898/ark#6, and thank you @eitsupi for the suggestion. We do not want to take on this dependency in Ark, now or as a follow-up in the near term. That helpful benchmark in the prototype (thank you!) shows a small gain for the first suggestions, but the first search becomes slower. The indexing also stays on the R thread. For this small change in performance, I'm not up for us taking on a new dependency. Let's keep Ark on native help.search(), and remove the follow-up plan from the PR description.
2. A new import rebuilds the full Python index
The cache key for HelpIndex includes the snapshot of loaded modules. Thus, each new import in the session discards the full filesystem index. I measured this in an example data science environment with approximately 28k topics:
| Scenario | Time on the kernel thread |
|---|---|
| First keystroke (cold build) | 2-4s, and the index builds two times (see below) |
| Later keystrokes | approximately 60 ms |
First keystroke after import pandas |
approximately 3.1s (full rebuild) |
In a realistic data science session, users import new packages all the time. Thus, the first help search after most imports blocks user code for some seconds. I suggest we approach with two layers.
- Cache the filesystem index with only
sys.pathand the file fingerprints as its key. - Then, for each request, apply the snapshot of loaded modules on top of that index. (This step is fast because it does not walk the filesystem.)
3. The first build always runs two times
When tokenize.detect_encoding and decode read source files, Python imports encodings.* modules (for example, encodings.latin_1 and encodings.big5). These imports change the snapshot of loaded modules. Thus, the first update_context() after the first build discards the new index. The fix for item 2 also removes this problem.
4. An environment that is too large gives no index
When discovery goes past max_locations, Discovery.entries raises a RuntimeError. No code catches this error, so the full build fails and the user gets no suggestions. I think a partial index with a diagnostic is better than no index.
5. R search with regex characters
.ps.help.searchHelp gives the raw query to utils::help.search(), which reads it as a regular expression. Do queries such as [.data.frame or c( cause an error?
I'll do a detailed review of the frontend code after these changes lands, and then when the design is ready, we will move this branch into the main repository to run the full CI and merge it. (Your commits and credit will stay the same.)
Thank you again for all of your work on this! 🙌
|
Thanks, @juliasilge! I’ve revised both PRs according to your suggestions:
I added regression coverage and tested the revised behavior in Positron Dev, including reproducing the R errors before the fix and confirming the results afterward. Initial indexing remains synchronous, as documented. These changes are ready for another review, including the frontend review you mentioned. Thank you! |
…ata (#33) Keep filesystem/ZIP discovery cached across ordinary imports and codec loading, then overlay loaded-module metadata and separately cache dynamic package paths. Keep docstring invalid-escape warnings out of the user console while preserving warning settings. Validated with targeted index tests, Ruff checks, and manual testing in the development build.
Keep already-discovered topics when the traversal budget is exhausted, record an incomplete-index diagnostic, and retain partial results in the cache. Avoid guessing package and namespace precedence across unvisited directories. Validated with 44 combined index tests, Ruff checks, and manual development-build testing with a reduced discovery budget.
f241ff2 to
5e7c21f
Compare
Dependency
Requires posit-dev/ark#1401 to merge, followed by Positron adopting the resulting upstream Ark version through its normal update process. This PR intentionally does not change the Ark submodule pointer. Until that upstream update lands, the checked-in Ark does not support the new Help protocol. Local R validation used the companion Ark checkout at
f645159d, kept outside the committed Positron gitlink.Summary
Adds interpreter-wide search and autocomplete to the first row of the Help pane, retaining in-page search on the second row. Resolves #422.
help.search()semantics and rendering, selected-topic resolution, and narrow-pane layout are retained.Scope and limitations
Index construction/rebuild remains synchronous; this does not add background indexing or service R Help requests while user code is running. Python may omit runtime-only summaries and dynamically exposed paths from unloaded packages.
#4484 is acknowledged and unchanged; its refresh/history behavior is left to the Positron team. F1 lookup changes remain outside this PR.
Protocol:
get_help_topics(query, limit),search_help(query, search_id), and optionalsearch_idonshow_help. Coordinate the frontend and both backend versions when landing.Demo
search.mp4
Verification
npm run typecheck-clientand client transpilation passed.git diff --checkpassed. Targeted frontend lint has no errors; warnings remain.These are local results; upstream CI is not yet green.