Skip to content

fix: proxy delay test reports empty/flickering latency and does nothi… - #836

Open
Code-0-0 wants to merge 1 commit into
MetaCubeX:mainfrom
Code-0-0:fix/delay-test
Open

Code-0-0 wants to merge 1 commit into
MetaCubeX:mainfrom
Code-0-0:fix/delay-test

Conversation

@Code-0-0

Copy link
Copy Markdown

Summary

The in-app delay test (the ⚡ button on the proxy page) is broken on top of current mihomo in two
independent ways:

  1. Latency values appear and disappear at random. Right after pressing ⚡, nodes that were just
    tested successfully frequently show no value at all, and the values change on every refresh.
  2. The button is a complete no-op for some groups — e.g. the auto-created GLOBAL group, and
    use:-only groups whose providers have no health-check.url. Nothing is tested, no delay is
    ever shown.

This patch makes the app resolve the test URL itself, run the test on the group's members with that
URL, and read the delay back with the same URL.

Root cause

1. The delay is looked up with a random test URL

mihomo keeps one delay history per test URL (extraDelayHistories) and LastDelayForTestUrl(url)
returns 0xffff unless the last test of that exact URL was alive
(adapter/adapter.go):

func (p *Proxy) LastDelayForTestUrl(url string) (delay uint16) {
	var maxDelay uint16 = 0xffff
	...
	if !alive || history.Delay == 0 {
		return maxDelay
	}
	return history.Delay
}

LastDelay() was removed from constant.Proxy (cc642972, implementation dropped in f63acc02),
so there is no URL-independent delay any more. core/src/main/golang/native/tunnel/proxies.go
therefore had to pick a URL — and picked it by iterating a Go map, whose iteration order is
randomized on every call, with the gstatic URL hard-coded as the fallback:

testURL := "https://www.gstatic.com/generate_204"
for k := range p.ExtraDelayHistories() {
	if len(k) > 0 {
		testURL = k
		break
	}
}
Delay: int(p.LastDelayForTestUrl(testURL)),

As soon as a proxy has been tested with more than one URL — very common, e.g.
proxy-providers.<x>.health-check.url differing from a group's url:, or one of those URLs being
blocked/hijacked for that node — the picked URL is a coin flip. Anything above Short.MAX_VALUE is
rendered as an empty string by ProxyViewState (if (proxy.delay in 0..Short.MAX_VALUE) ... else ""),
so online nodes show blank latency at random, and the result differs on every refresh.

2. provider.HealthCheck() silently does nothing when the provider has no test URL

HealthCheck() in connectivity.go only fanned out to the providers:

for _, pr := range g.Providers() {
	go pr.HealthCheck()
}

mihomo builds the reserved default provider — the only provider behind the auto-created GLOBAL
group — with an empty health check URL, and it can never get a registered URL (use: rejects
VehicleType() == Compatible, and it is created after the groups are parsed):

// mihomo config/config.go, parseProxies()
hc := provider.NewHealthCheck(ps, "", 5000, 0, true, nil)
pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc)

HealthCheck.execute() returns early for an empty URL, so zero proxies are tested:

url = strings.TrimSpace(url)
if len(url) == 0 {
	log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid)
	return
}

The same happens for use:-only groups without an explicit url: when the provider has no
health-check.url, because addTestUrlToProviders() is only reached in the else branch of
adapter/outboundgroup/parser.go.

How to reproduce

Before this patch

  1. Load a profile in which one proxy is health-checked with two or more distinct URLs — e.g. a
    proxy-providers entry with health-check.url: https://… while some group uses
    url: http://… — or a profile whose group provider has no test URL at all (a use:-only group
    without url:, or GLOBAL in global mode).
  2. Open the proxy page and press the ⚡ button; then switch tabs / scroll to force a refresh.
  3. Observed: latency numbers flicker away at random for nodes that are online, and for the groups
    above pressing ⚡ does nothing whatsoever. With log-level: debug, logcat only shows
    Health Check has been skipped due to testUrl is empty.

After this patch

  • ⚡ runs a real test over the group's members, the toolbar progress indicator ends, and the values
    stay stable across refreshes; GLOBAL works as well. Logcat shows
    Health checking group '<name>' with url '<url>' (N proxies).

Changes

File +/- Change
core/src/main/golang/native/tunnel/connectivity.go +62/−6 resolve the group's test URL (groupTestURL: the group-owned provider's HealthCheckURL(), then the first provider with a URL, finally C.DefaultTestURL) and call proxy.URLTest(ctx, url, nil) for every member (concurrency 10, 5s timeout per proxy) instead of relying on provider.HealthCheck()
core/src/main/golang/native/tunnel/proxies.go +48/−21 lastDelay() asks for the group's own URL first and only then falls back to any alive URL; no more random map pick. QueryProxyGroup() passes the resolved URL down to convertProxies()
core/src/main/golang/native/tunnel.go +2/−0 release the JNI global reference of nativeHealthCheck (C.release_object()), matching load() / updateProvider() — the previous code leaked one global ref per button press
app/src/main/java/com/github/kr328/clash/ProxyActivity.kt +14/−4 clear the url-testing state when the call fails, instead of leaving the toolbar indicator spinning forever
design/src/main/java/com/github/kr328/clash/design/ProxyDesign.kt +16/−0 new finishUrlTesting(position) used by that failure path (guards against a missing adapter / out-of-range index)

Total: 5 files, +142/−31.

Verification

  • Built with this repository's own Build Debug workflow in a fork (Go 1.26 + the NDK/JDK that CI
    provisions, all four ABIs): run succeeded, 5 APKs uploaded.
    https://github.com/Code-0-0/ClashMetaForAndroid/actions/runs/35441174195
  • Installed on a device and checked manually:
    • the ⚡ progress indicator ends normally and latency values are stable across tab switches and
      refreshes (previously they flickered away);
    • the GLOBAL group in global mode now performs a real test instead of doing nothing;
    • logcat shows Health checking group '<name>' with url '<url>' (N proxies) and no longer
      Health Check has been skipped due to testUrl is empty.

Notes for reviewers

  • The URL resolution mirrors mihomo's own parser rules, so a manual test shares both the URL and
    the delay history with the periodic health check of the same group/provider.
  • expected-status is not reachable through the public provider API, so a manual test passes nil
    for expectedStatus — the same behaviour as the REST API's /group/:name/delay without
    expected=. The periodic health check still applies the configured status.
  • Concurrency (10) and per-proxy timeout (5s) match provider.HealthCheck() defaults.

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