Skip to content

feat: report the runtime memory limit in the memory endpoint - #3050

Closed
Ember-Moth wants to merge 1 commit into
MetaCubeX:Alphafrom
Ember-Moth:feat/report-memory-limit
Closed

feat: report the runtime memory limit in the memory endpoint#3050
Ember-Moth wants to merge 1 commit into
MetaCubeX:Alphafrom
Ember-Moth:feat/report-memory-limit

Conversation

@Ember-Moth

Copy link
Copy Markdown

Problem

The /memory endpoint has always reported a hardcoded oslimit: 0:

OSLimit uint64 `json:"oslimit"` // maybe we need it in the future

Meanwhile the Go runtime's soft memory limit (GOMEMLIMIT or debug.SetMemoryLimit) has a substantial effect on mihomo's footprint. Measured on linux/amd64 with rule-sets of 112,332 + 26,822 domains and 5,687 CIDRs loaded, anonymous RSS:

steady state
no limit 15.4 MB
GOMEMLIMIT=37MiB 9.6 MB

On platforms with a hard memory ceiling — an Apple Network Extension gets roughly 50 MB before jetsam kills it, small routers get less — setting this limit is one of the most effective knobs available. But there is currently no way to confirm through the API that a limit is actually in effect: a typo in the environment variable and you silently run unlimited.

Change

Fill the field with the limit the runtime is actually enforcing:

  • memoryLimit() reads it via debug.SetMemoryLimit(-1) — a negative argument is the documented read-only form and does not modify the limit.
  • The runtime spells "no limit" as math.MaxInt64, which is not a useful number to hand a dashboard, so that case keeps reporting 0. Default behaviour is unchanged: anyone not setting a limit sees exactly what they see today.
  • The value is read inside the once-per-second push loop, so a limit changed at runtime is reflected. The read-only path briefly takes the heap lock and skips the pacer commit (runtime/mgcpacer.go, in < 0 branch); once per second per watcher this is negligible.

Dashboards consuming /memory will start receiving a non-zero oslimit when (and only when) the user has configured a limit — which is the point: inuse against oslimit is exactly the "how close am I to the ceiling" view.

Verification

Unit test covers the mapping in both directions plus the property that reading does not alter the limit (guarding the -1 read-only form against future edits).

End to end, same config, three runs:

/memory reports
no GOMEMLIMIT {"inuse":38199296,"oslimit":0}
GOMEMLIMIT=37MiB {"inuse":37253120,"oslimit":38797312}
GOMEMLIMIT=100MiB {"inuse":43155456,"oslimit":104857600}

38797312 = 37 × 1024², 104857600 = 100 × 1024².

Notes

This only reports the limit; it does not set one. Making the limit a first-class config option (as opposed to an environment variable) may be worth a separate discussion — this change is useful either way, since it also reports limits set via GOMEMLIMIT today.

The oslimit field of /memory has been hardcoded to 0 with a "maybe we need
it in the future" note. Report the soft limit the Go runtime is actually
enforcing, whether it came from GOMEMLIMIT or debug.SetMemoryLimit, so it
is possible to confirm a limit took effect.

The runtime spells "no limit" as math.MaxInt64, which is not a useful
number to hand a dashboard, so that case keeps reporting 0 and the default
behaviour is unchanged.

Measured with a rule-set of 112332 + 26822 domains and 5687 CIDRs, anonymous
RSS after load:

  no GOMEMLIMIT      15.4 MB
  GOMEMLIMIT=37MiB    9.6 MB

The limit changes the footprint substantially, which matters on platforms
with a hard memory ceiling such as an Apple network extension, but until now
there was no way to see whether one was in effect.
@wwqgtxx

wwqgtxx commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

However, as things stand, it is difficult to see any real utility in reporting this value to the API; it appears to serve only to verify whether your environment variables are set correctly, representing nothing more than an additional runtime cost.

@Ember-Moth

Copy link
Copy Markdown
Author

Fair enough on the utility — I'll close this.

Two things worth leaving on the record though. The cost is smaller than it looks: debug.SetMemoryLimit(-1) measures 15.8 ns uncontended and 18.9 ns with four goroutines hammering the allocator for mheap_.lock, against the os.ReadFile("/proc/pid/statm") that t.Memory() already does every tick in the same loop — a few orders of magnitude more. So the added cost is noise relative to what the handler already pays.

And you're right that reading it per tick was unjustified: nothing in mihomo changes the limit at runtime, so I was designing for a capability that does not exist. Reading once at handler entry would have been correct.

The utility case is genuinely thin, though. GOMEMLIMIT works today without this, and log-level: debug already exposes pprof, which gives a far more useful picture than a single ceiling number. Closing — easy to revive if an inuse-against-ceiling view is ever wanted.

@Ember-Moth Ember-Moth closed this Jul 30, 2026
@Ember-Moth
Ember-Moth deleted the feat/report-memory-limit branch July 30, 2026 08:33
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.

2 participants