Batch RealDelay probes through an isolated Observatory core - #6050
Open
eliotcougar wants to merge 9 commits into
Open
Batch RealDelay probes through an isolated Observatory core#6050eliotcougar wants to merge 9 commits into
eliotcougar wants to merge 9 commits into
Conversation
Merge one delay-test batch into a disposable Xray process, honor the configured profile concurrency, and update each result as its observatory candidates finish.
Build batch configs directly from v2rayNG's typed speed-test models instead of reparsing and validating arbitrary JSON states the app does not generate. Keep custom configs and non-Observatory policy strategies on the existing individual delay path. Use the worker's single completion contract, retain only cancellation and pending-result state that can occur, and isolate subscription-update probes from the live VPN daemon process.
Reserve speed-test naming for future throughput testing and use HEAD as the default Observatory probe method.
Keep cancellation in the batch-test notification while the disposable probe process owns a single active worker. Starting another batch now requests replacement directly, allowing the service to suppress the old worker's completion before handing the new intent to a fresh process instead of emitting an explicit cancellation event that can clear the new UI state.
eliotcougar
marked this pull request as ready for review
August 9, 2026 21:17
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.
I would like to raise awareness to the issue with running bulk real-delay probes one xray-core per outbound inside the same process as the main core. Last time this proposal has been bundled together with some unrelated features. I tried to separate it cleanly, patched some edge cases (after I saw that guy with 500+ profiles in his app), cleaned-up the code in general, and collected some performance data. Hopefully, this time it will be considered when you have time for it.
Summary
This PR replaces the per-profile RealDelay hot path with a probe plan that runs compatible targets through one short-lived Xray core in a process separate from the VPN daemon.
It preserves progressive results, policy-group selection, cancellation, malformed-profile isolation, the existing RealDelay concurrency setting, subscription auto-test behavior, and fallback support for configurations that cannot be merged.
It depends on AndroidLibXrayLite draft PR 2dust/AndroidLibXrayLite#204.
Existing problem
The current RealDelay implementation creates one coroutine per profile on a limited dispatcher. For each ordinary profile it performs a raw TCP precheck, builds a complete configuration, creates/starts/closes a separate Xray core, and takes two GET samples.
For a large subscription this has four costs:
core.Instancevalues can overlap inside the same native process, including the process hosting the long-running VPN core.Why overlapping Xray cores in one process are unsafe
The Xray user documentation describes operational Xray instances/processes as independent, and its architecture diagram covers a single Xray process. The embedded Go API has additional process-wide state.
XTLS's libXray embedding documentation explicitly warns that Xray-core keeps its system dialer DNS client and outbound manager in process-wide state. Creating a temporary ping/test core can replace that state; closing the temporary core does not restore the previous values; overlapping instances therefore need separate processes.
The exact xray-core revision pinned by this branch (
5ca6f4b7d4dc) confirms it:core.New/core.NewWithContextcallsinternet.InitSystemDialerduring instance construction.dnsClientandobmare package globals.InitSystemDialeroverwrites them; the global outbound manager is also used fordialerProxyresolution.Instance.Closecloses the instance's features but does not restore the managers previously stored in those package globals.A temporary test core can therefore redirect DNS or
dialerProxylookup through another instance's managers, leave the long-running core with replaced process state, or leave globals pointing at features that have already closed.This PR changes
CoreTestServicefrom:RunSoLibV2RayDaemonto a disposable:Probeprocess. Interactive batches run one at a time, the process terminates after the batch, and a back-to-back replacement request is redelivered to a fresh process. Subscription update/testing also moves to:SubscriptionUpdate, separate from the VPN daemon.Process isolation protects the live tunnel even when an unmergeable profile must use the legacy per-profile fallback. The optimized path goes further and avoids overlapping temporary cores by combining compatible targets into one Xray instance.
Proposed flow
Probe-plan construction
CoreConfigContextBuildernow has a probe-specific path that resolves only the outbound dependency graph required for testing. It skips unrelated routing/DNS/runtime sections and shares a lazily decoded profile/subscription snapshot across the complete batch.For every compatible source configuration,
ProbeConfigBuilder:streamSettings.sockopt.dialerProxyreferences to the namespaced dependency;Custom Xray JSON, malformed dependency graphs, unsupported balancer strategies, and policy groups with unsupported fallback semantics are kept out of the combined config and tested through the individual compatibility path.
If Xray rejects a combined configuration despite per-source validation, the app recursively divides only the still-pending profile set. Valid subsets continue through shared cores; an irreducible single profile falls back individually. One bad profile therefore cannot discard the rest of the batch.
Concurrency and policy groups
The existing Concurrent RealDelay tests setting is passed to one native worker pool. Each worker performs one unchanged upstream
BurstObservatory.Checkfor one outbound tag.The limit is therefore enforced over actual network probes, not merely over visible policy-group rows.
Progressive results and UI cost
AndroidLib emits a serialized callback when a target completes. A policy group may publish a new selected delay while other candidates remain, and receives a final
completed=trueupdate after its last candidate.The app persists each result immediately and broadcasts a
RealPingResult(guid, delay)DTO. Including the GUID removes the previous identity-less “something changed” event that forced a full group reload.MainViewModelapplies result DTOs directly to cached rows. It drains results through one serialized 500 ms loop:Finishwaits for any scheduled drain and forces the final pending flush before completing the UI state.The service-event buffer is enlarged for large bursts, while the persisted MMKV values remain authoritative. Progress notifications are throttled separately and count physical work units.
Cancellation and failure behavior
core.NewWithContext, so cancellation reaches active Observatory checks.-1result.End-to-end 500-profile benchmark
Conditions:
/generate_204fixtures with a fixed 100 ms response delaydumpsys meminfosampled approximately once per second739e303f+ AndroidLibb2138986b6fbe5e5+ AndroidLib484a8771Values are medians of three runs; parentheses are observed min-max ranges.
All six measured runs completed all 500 targets without crashes, ANRs, probe failures, or connection timeouts. Upstream performed 500 TCP prechecks followed by 1,000 GET samples; the proposed path performed exactly 500 HEAD samples.
The 3.330-second measured HTTP phase is only about 6.6% above the 3.125-second theoretical floor for 500 targets, 16 workers, and 100 ms responses. Increasing concurrency is therefore not the useful next optimization.
The tradeoff is startup latency: building the complete dependency plan delays the first result by about 0.73 seconds. Caching or directly constructing minimal outbound fragments is the principled follow-up.
500 ms result-flush benchmark
The end-to-end comparison above used the earlier 50 ms coalescing window. A separate finish-aware emulator benchmark compared the pushed 50 ms version (
d30745cc) with the same source using the final 500 ms interval.Conditions: Pixel 5 x86_64 AVD, Android 11 / API 30, Play Store debug, 500 rows, concurrency waves of 16, three measured repetitions after warm-up. Every run included the production
Finishevent.FinishThe deliberate UI tradeoff is up to roughly half a second before the first partial row update. The final batch completion is not extended because
Finishforces the pending flush.Benchmark boundaries
5 s * ceil(targets / 16)network time: about 160 seconds for 500 targets if every worker times out.b6fbe5e5/484a8771. The final review tips (2f9b560b/93273c7) retain that architecture and add lifecycle/cancellation fixes and cleanup.Dependency and review setup
93273c799e258e02905c466ac5d6230a9d1583072f9b560b2cc08be371985103e9c682b60877979dThe generated
libv2ray.aaris intentionally not committed. Until the AndroidLib PR is merged and the submodule/AAR is updated upstream, review builds must build the AAR from PR #204 and place it inV2rayNG/app/libs/libv2ray.aar.Validation
I have been running the earlier version of this code without protections against broken configurations. I experienced no failures.
Completed against the final reviewed tips:
ProbeConfigBuilderTestgo test -race ./...go vet ./...:app:assemblePlaystoreDebuggit diff --checkThe focused Kotlin and Go test files are retained locally for continued development but intentionally are not part of either PR diff.