Skip to content

Add progressive RealDelay probing through one Xray instance - #204

Open
eliotcougar wants to merge 8 commits into
2dust:mainfrom
eliotcougar:fix/observatory-delay-probing
Open

Add progressive RealDelay probing through one Xray instance#204
eliotcougar wants to merge 8 commits into
2dust:mainfrom
eliotcougar:fix/observatory-delay-probing

Conversation

@eliotcougar

@eliotcougar eliotcougar commented Aug 9, 2026

Copy link
Copy Markdown

Summary

This PR proposes a small gomobile API for progressive RealDelay probing through one short-lived Xray instance.

It deliberately does not add or require any xray-core API. The implementation is an adapter over the existing, unchanged:

  • extension.BurstObservatory.Check
  • extension.Observatory.GetObservation
  • routing.BalancerPrincipleTarget.GetPrincipleTarget

The companion v2rayNG branch runs this API outside the long-running VPN process, supplies a compact combined configuration, and consumes progressive results.

Proposed API

API Contract
NewProbeController() Creates the cancellation owner for one probe operation.
Probe(configJSON, groupsJSON, maxConcurrency, handler) Starts one Xray instance, probes every listed target once, and blocks until completion or cancellation.
ProbeController.Cancel() Cancels the Xray instance context and active Observatory checks.
ProbeHandler.OnProbeResult(groupID, delay, completed) Delivers serialized progressive group updates; completed becomes true after that group's last physical target.

The group metadata is intentionally small: a stable caller ID, the outbound tags to check, and an optional balancer tag. Xray configuration remains ordinary JSON, so AndroidLib does not duplicate v2rayNG's profile/configuration model.

Execution model

  1. Parse the compact group metadata.
  2. Load the combined Xray configuration and create one instance with core.NewWithContext.
  3. Obtain the existing BurstObservatory feature from that instance.
  4. Interleave targets from different groups, so one large policy group cannot monopolize the queue.
  5. Run a fixed worker pool of at most maxConcurrency workers.
  6. Call Check([]string{tag}) once for each physical outbound target. A single-tag call is one Observatory sample regardless of least-load history settings.
  7. Coalesce closely spaced completions for 50 ms before reading the Observatory snapshot, reducing repeated full snapshot construction.
  8. For ordinary profiles, report that target's observed delay. For policy groups, ask the existing balancer principle for its current selected target and report that target's delay.
  9. Serialize callbacks, close the one temporary instance, and return.

The configured limit therefore caps active BurstObservatory.Check calls, including policy-group members. It is not merely a limit on visible UI rows.

Why not create one Xray core per target concurrently?

The risk is not ordinary Go concurrency by itself. It is overlapping *core.Instance objects created by multiple goroutines inside one native process.

The Xray user documentation describes operational Xray instances/processes as independent, and its architecture diagram is explicitly for a single Xray process. That does not mean independently embedded core.Instance values have no process-wide state.

XTLS's own libXray embedding documentation now states the boundary directly: Xray-core keeps the system dialer's DNS client and outbound manager in process-wide state; creating another instance may replace them; closing the temporary instance does not restore the previous values; callers that require overlap must use separate processes.

The pinned xray-core source (5ca6f4b7d4dc) shows why:

Consequences of overlapping temporary instances can include a lookup or dialerProxy operation consulting another instance's manager, a long-running core retaining pointers replaced by a temporary test core, or process globals still referencing a manager whose instance has already closed.

Serializing hundreds of independent cores would avoid overlap but retain hundreds of configuration parses, feature graphs, starts, HTTP clients, and closes. Combining compatible targets into one temporary core avoids both the state collision and most of that startup cost.

AndroidLib itself cannot create an Android process boundary. Callers must not overlap this temporary core with their long-running core in the same process. The companion v2rayNG PR runs interactive probes in a disposable :Probe process and subscription probes in a process separate from the VPN daemon.

Use of unchanged Burst Observatory

This adapter uses the existing Burst Observatory configuration. The upstream defaults and implementation already support HEAD requests, a five-second timeout, one-time Check calls, and observation snapshots.

Relevant unchanged source:

No forked Observatory interface, notification callback, batch primitive, or deadline extension is introduced.

Failure and cancellation semantics

  • Invalid group JSON or an invalid combined Xray configuration returns a normal error to the caller.
  • A panic originating from malformed native/configuration state is converted to an error so the app can isolate the offending profile.
  • Cancellation propagates through the context used to construct Xray, allowing active Burst Observatory work to stop rather than waiting for every timeout.
  • Callback calls are serialized even though checks are concurrent.
  • Failed targets report an unavailable delay through the ordinary Observatory snapshot; one failed target does not abort the worker pool.

The app owns higher-level retry/isolation policy because only it understands profile identity and can rebuild smaller configuration subsets.

End-to-end benchmark

The benchmark exercised the companion app, so these numbers measure the complete proposal rather than this library in isolation.

Conditions:

  • Pixel 9 Pro x86_64 AVD, Android 17 / API 37, four virtual CPUs
  • Play Store debug builds signed with the same certificate
  • 500 deterministic SOCKS profiles
  • emulator-local SOCKS and HTTP fixtures; fixed 100 ms HTTP response delay
  • configured concurrency 16
  • one warm-up plus three measured runs per implementation
  • upstream app 739e303f + AndroidLib b2138986
  • Observatory app b6fbe5e5 + AndroidLib 484a8771

Values are medians of three runs:

Metric Current upstream One-core Observatory Change
Last probe response 8.430 s 5.036 s 40.3% faster
HTTP phase 7.410 s 3.330 s 55.1% shorter
Final UI state observed 10.635 s 7.160 s 32.7% sooner
Total app CPU time 21.22 s 3.54 s 83.3% less
VPN/test daemon CPU time 8.16 s 1.19 s 85.4% less
Peak combined PSS 221,383 KiB 166,836 KiB 24.6% / 53.3 MiB less
Peak daemon PSS 123,978 KiB 67,219 KiB 45.8% less
Peak Java heap 42,296 KiB 33,476 KiB 20.9% less

All six measured runs completed every target without crashes, ANRs, probe failures, or timeouts. The 3.330-second HTTP phase is close to the 3.125-second theoretical floor for 500 targets, 16 workers, and 100 ms responses.

Boundaries

  • Results scale with physical outbound targets, not visible profile count. A policy group may expand into many targets.
  • The benchmark covers healthy, batchable generated profiles. Custom configurations and unsupported balancer forms use the app's per-profile fallback and do not receive the same CPU/PSS benefit.
  • With 500 dead targets, five-second timeout, and concurrency 16, the bounded tail is approximately 5 s * ceil(500 / 16) = 160 s.
  • A very large combined dependency graph may need future sequential chunking based on target count or serialized configuration size.
  • The one-core API is for RealDelay measurement, not throughput/speed testing.

Validation

  • end-to-end local HTTP probe test with ordinary and policy-group targets
  • verified HEAD and one request per target
  • verified the exact concurrency ceiling
  • active cancellation test
  • go test -race ./...
  • go vet ./...
  • four-ABI gomobile AAR build
  • generated Java API inspection for ProbeController and ProbeHandler
  • companion Play Store debug build against the generated API

The focused test file is retained locally for continued development but is intentionally not included in this PR diff.

Companion integration

v2rayNG draft PR: 2dust/v2rayNG#6050.

The companion PR documents profile-plan construction, process isolation, fallback handling, UI result coalescing, and the UI-specific benchmark.

Drive unchanged BurstObservatory checks in bounded profile groups and publish only the affected profile result instead of repeatedly serializing complete batch snapshots.
Trust the typed v2rayNG probe plan and the fixed upstream BurstObservatory implementation instead of defending against duplicate plans, controller reuse, nil handlers, and impossible interface/result types. Remove the per-result acknowledgement barrier because the single completion consumer already serializes callbacks, while retaining bounded workers and real cancellation support.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant